W
O
R
D
L
E
E
M
B
E
R
o
n
c
e
...
B
U
I
L
D
s
A
P
P
L
O
s
E
S
@
A Developer's Journey
L
O
s
E
S
(for the first time)
So there I was...
January 18th, 2022
So there I was...
Proxy. The word was proxy.
down to my last guess
January 18th, 2022

P
R
O
?
?
o
f
So you lost at Wordle.
What now?
So you lost at Wordle.
What now?

I did not do that.
G
r
i
e
f
(processing loss)
We all grieve in our own way
['shock','denial','anger','bargaining','guilt','depression','acceptance']
.forEach(stage => process(stage));We all grieve in our own way
Failure often sparks inspiration
['shock','denial','anger','bargaining','guilt','depression','acceptance']
.forEach(stage => process(stage));So I got an idea
So I got an idea
What if I were to build something that could suggest words based on previous guesses?
How to cheat at wordle
How to not lose at wordle?
How to have fun playing wordle.
How to cheat at wordle
How to not lose at wordle?
How to have fun playing wordle.
How to have fun

Finding joy through Ember
Finding joy through Ember
Using engineering as a medium for creativity.

Finding joy through Ember
Using engineering as a medium for creativity.




Coding can be an act of creativity
I
D
E
A
S
(starting out)
Definition of an Idea
What if I were to build something that could suggest words based on previous guesses?
Given input letters of three specific types (correct, included, excluded), I should be able to derive a subset of five letter words that are possible with the input data.
Minimum Requirements
But first I needed words
But first I needed words
Lots of words.
Research and Resources
12,974
Research and Resources
5,046
12,974
five letter words in the English language (approximately)
Research and Resources
five letter words in the English language (approximately)
5,046
12,974
of those are commonly used (not including proper names)
Research and Resources
are more commonly used (not including proper names)
five letter words in the English language (approximately)
12,974
5,046
Research and Resources
are more commonly used (not including proper names)
five letter words in the English language (approximately)
12,974
5,046
10,657
Research and Resources
words used for Wordle app word validiation
are more commonly used (not including proper names)
5,046
five letter words in the English language (approximately)
12,974
10,657
2,315
Research and Resources
words used for Wordle app answers
five letter words in the English language (approximately)
12,974
are more commonly used (not including proper names)
5,046
words used for Wordle app word validiation
10,657
2,315
Research and Resources
five letter words in the English language (approximately)
12,974
are more commonly used (not including proper names)
5,046
10,657
+ 2,315
12,972
Research and Resources
12,974
12,972
Included Letters
- Letter is included in the word
- Letter is in the incorrect position
Excluded Letters
- Letter is not included in the word
The Idea
E
M
B
E
R
Correct Letters
- Letter is included in the word
- Letter is in the correct position
The Idea
Included Letters
Excluded Letters
Correct Letters
E
M
B
E
R
EMBER
REACT
EGRET
LEARN
EARTH
EXAMS
BEARS
ENARM
The Idea
Included Letters
Excluded Letters
Correct Letters
- Known position lets us exclude any word where this letter IS NOT at that position
E
M
B
E
R
EMBER
REACT
EGRET
LEARN
EARTH
EXAMS
BEARS
ENARM
E
E
E
E
E
The Idea
Included Letters
- Known position lets us exclude any word where this letter IS at that position
- Also exclude words that don't include the letter
Excluded Letters
Correct Letters
- Known position lets us exclude any word where this letter IS NOT at that position
E
M
B
E
R
EMBER
REACT
EGRET
LEARN
EARTH
EXAMS
BEARS
ENARM
M
M
M
The Idea
Included Letters
- Known position lets us exclude any word where this letter IS at that position
Excluded Letters
- Any word containing an excluded letter at any position can be excluded
Correct Letters
- Known position lets us exclude any word where this letter IS NOT at that position
E
M
B
E
R
EMBER
REACT
EGRET
LEARN
EARTH
EXAMS
BEARS
ENARM
EXAMS
The Idea
Included Letters
- Known position lets us exclude any word where this letter IS at that position
Excluded Letters
- Any word containing an excluded letter at any position can be excluded
Correct Letters
- Known position lets us exclude any word where this letter IS NOT at that position
E
M
B
E
R
EMBER
REACT
EGRET
LEARN
EARTH
BEARS
ENARM
EXAMS
The Idea
Included
Excluded
Correct
E
M
B
E
R
[
'ember',
'embe',
'embr',
'emb',
'emer',
'eme',
'emr',
'em',
'eber',
'ebe',
'ebr',
'eb',
'eer',
'ee',
'er',
'e'
][
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
][
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
'mr',
'm',
'r'
]function buildKeys (word) {
const letters = word.split('').sort();
const indexes = [
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
];
const groupkeys = [];
for (let i = 0, len = letters.length; i < len; i++) {
for (const v of indexes) {
const keys = v.slice().split('');
const result = [];
for (const k of keys) {
result.push(letters[i + k * 1]);
}
groupkeys.push(result.join(''));
}
}
return [word, [...new Set(groupkeys)]];
}[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
]0 +
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
1 +
'0123',
'012',
'013',
'01',
'023',
'02',
'03',
'0',
2 +
'012',
'01',
'02',
'0',
3 +
'01',
'0',
4 +
'0',Heap's algorithm
The Idea
Included
Excluded
Correct
E
M
B
E
R
[
'ember',
'embe',
'embr',
'emb',
'emer',
'eme',
'emr',
'em',
'eber',
'ebe',
'ebr',
'eb',
'eer',
'ee',
'er',
'e'
][
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
][
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
'mr',
'm',
'r'
]function buildKeys (word) {
const letters = word.split('').sort();
const indexes = [
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
];
const groupkeys = [];
for (let i = 0, len = letters.length; i < len; i++) {
for (const v of indexes) {
const keys = v.slice().split('');
const result = [];
for (const k of keys) {
result.push(letters[i + k * 1]);
}
groupkeys.push(result.join(''));
}
}
return [word, [...new Set(groupkeys)]];
}[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
]0 +
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
1 +
'0123',
'012',
'013',
'01',
'023',
'02',
'03',
'0',
2 +
'012',
'01',
'02',
'0',
3 +
'01',
'0',
4 +
'0',Heap's algorithm
Recursive generators
The Idea
Included
Excluded
Correct
E
M
B
E
R
[
'ember',
'embe',
'embr',
'emb',
'emer',
'eme',
'emr',
'em',
'eber',
'ebe',
'ebr',
'eb',
'eer',
'ee',
'er',
'e'
][
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
][
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
'mr',
'm',
'r'
]function buildKeys (word) {
const letters = word.split('').sort();
const indexes = [
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
];
const groupkeys = [];
for (let i = 0, len = letters.length; i < len; i++) {
for (const v of indexes) {
const keys = v.slice().split('');
const result = [];
for (const k of keys) {
result.push(letters[i + k * 1]);
}
groupkeys.push(result.join(''));
}
}
return [word, [...new Set(groupkeys)]];
}[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
]0 +
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
1 +
'0123',
'012',
'013',
'01',
'023',
'02',
'03',
'0',
2 +
'012',
'01',
'02',
'0',
3 +
'01',
'0',
4 +
'0',Recursive generators
Yieldables
The Idea
Included
Excluded
Correct
E
M
B
E
R
[
'ember',
'embe',
'embr',
'emb',
'emer',
'eme',
'emr',
'em',
'eber',
'ebe',
'ebr',
'eb',
'eer',
'ee',
'er',
'e'
][
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
][
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
'mr',
'm',
'r'
]function buildKeys (word) {
const letters = word.split('').sort();
const indexes = [
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
];
const groupkeys = [];
for (let i = 0, len = letters.length; i < len; i++) {
for (const v of indexes) {
const keys = v.slice().split('');
const result = [];
for (const k of keys) {
result.push(letters[i + k * 1]);
}
groupkeys.push(result.join(''));
}
}
return [word, [...new Set(groupkeys)]];
}[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
]0 +
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
1 +
'0123',
'012',
'013',
'01',
'023',
'02',
'03',
'0',
2 +
'012',
'01',
'02',
'0',
3 +
'01',
'0',
4 +
'0',Yieldables
m
o
d
e
l
(prototype)
Proof of Concept
I started out building a simple class in node that took structured json input and provided an array of possible words as output.
b
r
a
i
n
(the algorithm)
Al Gore Rhythm

i
n
p
u
t
(data format)
The Input
Included
Excluded
Correct
{
correct: {
"0": "e"
},
included: {
"1": ["m"],
},
excluded: [
"b", "e", "r"
]
}E
M
B
E
R
The Input
Included
Excluded
Correct
{
correct: {
"0": "e"
},
included: {
"1": ["m"],
},
excluded: [
"b", "e", "r"
]
}E
M
B
E
R
The Input
Included
Excluded
Correct
{
correct: {
"0": "e"
},
included: {
"1": ["m"],
},
excluded: [
"b", "e", "r"
]
}E
M
B
E
R
p
r
o
b
e
(word lookup)
The Lookup
E
M
B
E
R
The Lookup
E
M
B
E
R
E
M
B
E
E
M
B
r
E
e
B
r
E
m
e
r
Build every possible letter combination for indexing words
The Lookup
Build every possible letter combination for indexing words
E
M
B
E
R
E
M
E
R
M
E
R
M
R
R
The Lookup
Build every possible letter combination for indexing words
E
M
B
E
R
E
M
E
R
M
E
R
M
R
R
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
]The Lookup
Build every possible letter combination for indexing words
E
M
B
E
R
E
M
E
R
M
E
R
M
R
R
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
][
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
]The Lookup
Build every possible letter combination for indexing words
E
M
B
E
R
E
M
E
R
M
E
R
M
R
R
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
][
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
][
'emr',
'em',
'er',
'e',
]The Lookup
Build every possible letter combination for indexing words
E
M
B
E
R
E
M
E
R
M
E
R
M
R
R
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
][
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
][
'emr',
'em',
'er',
'e',
][
'mr',
'm',
]The Lookup
Build every possible letter combination for indexing words
E
M
B
E
R
E
M
E
R
M
E
R
M
R
R
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
][
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
][
'mr',
'm',
][
'emr',
'em',
'er',
'e',
][
'r',
]The Lookup
Build every possible letter combination for indexing words
E
M
B
E
R
E
M
E
R
M
E
R
M
R
R
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
][
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
][
'mr',
'm',
][
'r',
][
'emr',
'em',
'er',
'e',
]The Lookup
E
M
B
E
Build every possible letter combination for indexing words
E
M
B
E
R
E
M
E
R
M
E
R
M
R
R
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
'mr',
'm',
'r'
][
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
][
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
][
'mr',
'm',
][
'r',
][
'emr',
'em',
'er',
'e',
]The Lookup
Build every possible letter combination for indexing words
E
M
B
E
R
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
'mr',
'm',
'r'
][
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
]Sorting the original word normalizes so we can easily discard duplicates.
I extracted a pattern from the results so I can cut out the calculation every time
E
M
B
E
R
E
M
B
r
The Lookup
Build every possible letter combination for indexing words
a
M
B
E
R
E
M
B
E
R
E
M
B
r
The Lookup
Build every possible letter combination for indexing words
a
M
B
E
R
{
'bemr': [
'amber',
'bream',
'ember',
'umber'
]
}B
r
e
a
m
E
M
B
E
R
E
M
B
r
a
The Lookup
Build every possible letter combination for indexing words
a
M
B
E
R
{
'abemr': [
'amber',
'bream',
]
}B
r
e
a
m
The Lookup
E
M
B
E
R
Build every possible letter combination for indexing words
[
'r', 're', 'rem', 'remb', 'rembe',
'e', 'er', 'ere', 'erem', 'eremb',
'b', 'be', 'ber', 'bere', 'berem',
'r', 're', 'rem', 'remb', 'rembe',
'e', 'em', 'emb', 'embe', 'ember'
]const keys = [];
const wordLetters = 'ember'.split('')
const combos = wordLetters.map((letter) => {
const i = wordLetters.indexOf(letter);
const len = wordLetters.length - 1;
return [
...wordLetters.slice(i * -1 - 1),
...wordLetters.slice(0, len - (i - len) - len),
];
});
combos.forEach((combo) => {
combo.reduce((acc, letter) => {
const key = ''.concat(acc || '', letter);
keys.push(key);
return key;
}, '');
});
const sortedKeys = keys.map((val) => {
return val.split('').sort().join('');
});
[...new Set(sortedKeys)].forEach((key) => {
const val = this.groupKeys.get(key) || [];
val.push(word);
this.groupKeys.set(key, val);
});['r', 'er', 'emr', 'bemr', 'beemr',
'e', 'eer', 'eemr', 'b', 'be', 'ber',
'beer', 'em', 'bem', 'beem']The Lookup
E
M
B
E
R
[
'ember',
'embe',
'embr',
'emb',
'emer',
'eme',
'emr',
'em',
'eber',
'ebe',
'ebr',
'eb',
'eer',
'ee',
'er',
'e'
][
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
]Build every possible letter combination for indexing words
Sorting the original word normalizes so we can easily discard duplicates.
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
]I extracted a pattern from the results so I can cut out the calculation every time
function buildKeys (word) {
const letters = word.split('').sort();
const indexes = [
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
];
const groupkeys = [];
for (let i = 0, len = letters.length; i < len; i++) {
for (const v of indexes) {
const keys = v.slice().split('');
const result = [];
for (const k of keys) {
result.push(letters[i + k * 1]);
}
groupkeys.push(result.join(''));
}
}
return [word, [...new Set(groupkeys)]];
}The Lookup
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
]0 + beemr
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',function buildKeys (word) {
const letters = word.split('').sort();
const indexes = [
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
];
const groupkeys = [];
for (let i = 0, len = letters.length; i < len; i++) {
for (const v of indexes) {
const keys = v.slice().split('');
const result = [];
for (const k of keys) {
result.push(letters[i + k * 1]);
}
groupkeys.push(result.join(''));
}
}
return [word, [...new Set(groupkeys)]];
}The Lookup
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
]0 + beemr
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
1 + eemr
'0123',
'012',
'013',
'01',
'023',
'02',
'03',
'0',function buildKeys (word) {
const letters = word.split('').sort();
const indexes = [
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
];
const groupkeys = [];
for (let i = 0, len = letters.length; i < len; i++) {
for (const v of indexes) {
const keys = v.slice().split('');
const result = [];
for (const k of keys) {
result.push(letters[i + k * 1]);
}
groupkeys.push(result.join(''));
}
}
return [word, [...new Set(groupkeys)]];
}The Lookup
[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
'mr',
'm',
]0 + beemr
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
1 + eemr
'0123',
'012',
'013',
'01',
'023',
'02',
'03',
'0',
2 + emr
'012',
'01',
'02',
'0',[
'beemr',
'beem',
'beer',
'bee',
'bemr',
'bem',
'ber',
'be',
'bmr',
'bm',
'br',
'b',
'eemr',
'eem',
'eer',
'ee',
'emr',
'em',
'er',
'e',
'mr',
'm',
'r'
]function buildKeys (word) {
const letters = word.split('').sort();
const indexes = [
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
];
const groupkeys = [];
for (let i = 0, len = letters.length; i < len; i++) {
for (const v of indexes) {
const keys = v.slice().split('');
const result = [];
for (const k of keys) {
result.push(letters[i + k * 1]);
}
groupkeys.push(result.join(''));
}
}
return [word, [...new Set(groupkeys)]];
}0 + beemr
'01234',
'0123',
'0124',
'012',
'0134',
'013',
'014',
'01',
'0234',
'023',
'024',
'02',
'034',
'03',
'04',
'0',
1 + eemr
'0123',
'012',
'013',
'01',
'023',
'02',
'03',
'0',
2 + emr
'012',
'01',
'02',
'0',
3 + mr
'01',
'0',
4 + r
'0',The Lookup
B
U
I
L
D
(user interface)
E
M
B
E
R
(is awesome)
E
M
B
E
R
(is awesome)
Ember makes building a reactive app easy
Purpose built systems driven by state flowing through the app, all powered by tracked properties
T
R
A
C
K
(autotracking)
Auto tracking is the foundation

Auto tracking is the foundation

Letter
(class)
Tray
(class)
Finder
(class)
Words
(class)
Word
(service)
Settings
(service)
reactive state anywhere in the app
Tile
(component)
Tray
(component)

Tray
- Represents letter state options
- Is drop target for Tile components
- Renders tile components within using letter position state
(component)

Tray
- Represents letter state options
- Is drop target for Tile components
- Renders tile components within using letter position state
(component)

Tray
- Represents letter state options
- Is drop target for Tile components
- Renders tile components within using letter position state
(component)

Tray
- Represents letter state options
- Is drop target for Tile components
- Renders tile components within using letter position state
(component)
Tray
- Tracks letters in tray
- Provides methods to add and remove letters from tray
(class)
<DraggableObjectTarget @action={{fn @api.updateLetter @id}} tabindex="0"{{on "click" (fn @api.trayClick @id)}} ...attributes>
{{#each this.letters as |letter|}}
<Letter @value={{letter}} @trayId={{@id}} @api={{@api}} @handleClick={{fn @api.tileClick letter @id}} />
{{/each}}
{{yield}}
</DraggableObjectTarget>
Tray
- Represents letter state options
- Is drop target for Tile components
- Renders tile components within using letter position state
(component)
get letters() {
const { api, id, letters } = this.args;
return letters || api.wordFinder.trays.get(id).items;
}<Tray @id="included0" @api={{this.api}} /><Tray @id="idle" @api={{this.api}} @letters={{this.api.wordFinder.keyboardLetters}} /><DraggableObjectTarget @action={{fn @api.updateLetter @id}} tabindex="0"{{on "click" (fn @api.trayClick @id)}} ...attributes>
{{#each this.letters as |letter|}}
<Letter @value={{letter}} @trayId={{@id}} @api={{@api}} @handleClick={{fn @api.tileClick letter @id}} />
{{/each}}
{{yield}}
</DraggableObjectTarget>
Tray
- Represents letter state options
- Is drop target for Tile components
- Renders tile components within using letter position state
(component)
get letters() {
const { api, id, letters } = this.args;
return letters || api.wordFinder.trays.get(id).items;
}<Tray @id="included0" @api={{this.api}} /><Tray @id="idle" @api={{this.api}} @letters={{this.api.wordFinder.keyboardLetters}} />Tray
- Tracks letters in tray
- Provides methods to add and remove letters from tray
(class)
clearItems = () => {
this.setItems([]);
};
setItems = (items) => {
this.items = new TrackedArray(items);
};
addItem = (item) => {
if (!this.items.includes(item)) this.items.push(item);
};
removeItem = (item) => {
if (this.items.includes(item)) return this.items.splice(this.items.indexOf(item), 1);
};







Tile
- Represents a letter tile
- Is draggable and droppable
- Uses Letter class as model
- Renders with letter location specific color
(component)


Tile
- Represents a letter tile
- Is draggable and droppable
- Uses Letter class as model
- Renders with letter location specific color
(component)

Letter
- Tracks letter state
- Holds letter location(s)
- Provides location getter/setter
(class)

Tile
- Represents a letter tile
- Is draggable and droppable
- Uses Letter class as model
- Renders with letter location specific color
(component)

<DraggableObject
@dragStartHook={{this.dragStartHook}}
@dragEndHook={{this.dragEndHook}}
@isDraggable={{this.isDraggable}}
@content={{@value}}
data-letter={{@value}}
role="button"
class={{concat "letter-object " this.letterBg (if this.showSelected " selected")}}
tabindex="0"
aria-label={{concat @value " tile " this.location}}
{{on "click" this.handleClick}}
...attributes>
{{@value}}
</DraggableObject> get letter() {
const { api, value } = this.args;
return api.wordFinder.words.letterData.get(value);
}
@cached
get from() {
return Array.isArray(this.letter.location) ? this.letter.location[0] : this.letter.location;
}
get location() {
const match = this.from.match(/([a-z])([0-9])/);
const key = match ? match[1] : this.from;
return locations[key];
}
get letterBg() {
const match = this.from.match(/([a-z])([0-9])/);
if (match) {
return `bg-letter-${match[1]}`;
}
return `bg-letter-${this.from}${this.autoExcluded ? ' autoExcluded' : ''}`;
}
get isDraggable() {
return !this.from.includes('d') && !this.autoExcluded;
}
get showSelected() {
return this.letter.isSelected && this.args.trayId === 's';
}
Tile
- Represents a letter tile
- Is draggable and droppable
- Uses Letter class as model
- Renders with letter location specific color
(component)

<DraggableObject
@dragStartHook={{this.dragStartHook}}
@dragEndHook={{this.dragEndHook}}
@isDraggable={{this.isDraggable}}
@content={{@value}}
data-letter={{@value}}
role="button"
class={{concat "letter-object " this.letterBg (if this.showSelected " selected")}}
tabindex="0"
aria-label={{concat @value " tile " this.location}}
{{on "click" this.handleClick}}
...attributes>
{{@value}}
</DraggableObject> get letter() {
const { api, value } = this.args;
return api.wordFinder.words.letterData.get(value);
}
@cached
get from() {
return Array.isArray(this.letter.location) ? this.letter.location[0] : this.letter.location;
}
get location() {
const match = this.from.match(/([a-z])([0-9])/);
const key = match ? match[1] : this.from;
return locations[key];
}
get letterBg() {
const match = this.from.match(/([a-z])([0-9])/);
if (match) {
return `bg-letter-${match[1]}`;
}
return `bg-letter-${this.from}${this.autoExcluded ? ' autoExcluded' : ''}`;
}
get isDraggable() {
return !this.from.includes('d') && !this.autoExcluded;
}
get showSelected() {
return this.letter.isSelected && this.args.trayId === 's';
}
Tile
- Represents a letter tile
- Is draggable and droppable
- Uses Letter class as model
- Renders with letter location specific color
(component)

<DraggableObject
@dragStartHook={{this.dragStartHook}}
@dragEndHook={{this.dragEndHook}}
@isDraggable={{this.isDraggable}}
@content={{@value}}
data-letter={{@value}}
role="button"
class={{concat "letter-object " this.letterBg (if this.showSelected " selected")}}
tabindex="0"
aria-label={{concat @value " tile " this.location}}
{{on "click" this.handleClick}}
...attributes>
{{@value}}
</DraggableObject> get letter() {
const { api, value } = this.args;
return api.wordFinder.words.letterData.get(value);
}
@cached
get from() {
return Array.isArray(this.letter.location) ? this.letter.location[0] : this.letter.location;
}
get location() {
const match = this.from.match(/([a-z])([0-9])/);
const key = match ? match[1] : this.from;
return locations[key];
}
get letterBg() {
const match = this.from.match(/([a-z])([0-9])/);
if (match) {
return `bg-letter-${match[1]}`;
}
return `bg-letter-${this.from}${this.autoExcluded ? ' autoExcluded' : ''}`;
}
get isDraggable() {
return !this.from.includes('d') && !this.autoExcluded;
}
get showSelected() {
return this.letter.isSelected && this.args.trayId === 's';
}
Tile
- Represents a letter tile
- Is draggable and droppable
- Uses Letter class as model
- Renders with letter location specific color
(component)

<DraggableObject
@dragStartHook={{this.dragStartHook}}
@dragEndHook={{this.dragEndHook}}
@isDraggable={{this.isDraggable}}
@content={{@value}}
data-letter={{@value}}
role="button"
class={{concat "letter-object " this.letterBg (if this.showSelected " selected")}}
tabindex="0"
aria-label={{concat @value " tile " this.location}}
{{on "click" this.handleClick}}
...attributes>
{{@value}}
</DraggableObject> get letter() {
const { api, value } = this.args;
return api.wordFinder.words.letterData.get(value);
}
@cached
get from() {
return Array.isArray(this.letter.location) ? this.letter.location[0] : this.letter.location;
}
get location() {
const match = this.from.match(/([a-z])([0-9])/);
const key = match ? match[1] : this.from;
return locations[key];
}
get letterBg() {
const match = this.from.match(/([a-z])([0-9])/);
if (match) {
return `bg-letter-${match[1]}`;
}
return `bg-letter-${this.from}${this.autoExcluded ? ' autoExcluded' : ''}`;
}
get isDraggable() {
return !this.from.includes('d') && !this.autoExcluded;
}
get showSelected() {
return this.letter.isSelected && this.args.trayId === 's';
}
Tile
- Represents a letter tile
- Is draggable and droppable
- Uses Letter class as model
- Renders with letter location specific color
(component)

<DraggableObject
@dragStartHook={{this.dragStartHook}}
@dragEndHook={{this.dragEndHook}}
@isDraggable={{this.isDraggable}}
@content={{@value}}
data-letter={{@value}}
role="button"
class={{concat "letter-object " this.letterBg (if this.showSelected " selected")}}
tabindex="0"
aria-label={{concat @value " tile " this.location}}
{{on "click" this.handleClick}}
...attributes>
{{@value}}
</DraggableObject> get letter() {
const { api, value } = this.args;
return api.wordFinder.words.letterData.get(value);
}
@cached
get from() {
return Array.isArray(this.letter.location) ? this.letter.location[0] : this.letter.location;
}
get location() {
const match = this.from.match(/([a-z])([0-9])/);
const key = match ? match[1] : this.from;
return locations[key];
}
get letterBg() {
const match = this.from.match(/([a-z])([0-9])/);
if (match) {
return `bg-letter-${match[1]}`;
}
return `bg-letter-${this.from}${this.autoExcluded ? ' autoExcluded' : ''}`;
}
get isDraggable() {
return !this.from.includes('d') && !this.autoExcluded;
}
get showSelected() {
return this.letter.isSelected && this.args.trayId === 's';
}Letter
- Tracks letter state
- Holds letter location(s)
- Provides location getter/setter
(class)

Tile
- Represents a letter tile
- Is draggable and droppable
- Uses Letter class as model
- Renders with letter location specific color
(component)

<DraggableObject
@dragStartHook={{this.dragStartHook}}
@dragEndHook={{this.dragEndHook}}
@isDraggable={{this.isDraggable}}
@content={{@value}}
data-letter={{@value}}
role="button"
class={{concat "letter-object " this.letterBg (if this.showSelected " selected")}}
tabindex="0"
aria-label={{concat @value " tile " this.location}}
{{on "click" this.handleClick}}
...attributes>
{{@value}}
</DraggableObject> get letter() {
const { api, value } = this.args;
return api.wordFinder.words.letterData.get(value);
}
@cached
get from() {
return Array.isArray(this.letter.location) ? this.letter.location[0] : this.letter.location;
}
get location() {
const match = this.from.match(/([a-z])([0-9])/);
const key = match ? match[1] : this.from;
return locations[key];
}
get letterBg() {
const match = this.from.match(/([a-z])([0-9])/);
if (match) {
return `bg-letter-${match[1]}`;
}
return `bg-letter-${this.from}${this.autoExcluded ? ' autoExcluded' : ''}`;
}
get isDraggable() {
return !this.from.includes('d') && !this.autoExcluded;
}
get showSelected() {
return this.letter.isSelected && this.args.trayId === 's';
}Letter
- Tracks letter state
- Holds letter location(s)
- Provides location getter/setter
(class)
@tracked controls;
@tracked settings;
@tracked locations;
constructor(letter, { settings = {}, controls = {} }) {
this.controls = controls;
this.settings = settings;
}
get location() {
return this.locations.length > 1 ? [...this.locations] : this.locations.join('');
}
// allows duplicates if all existing locations are in the good group
set location(val) {
const good = val.match(/(g)([0-9])/) && this.goodLocation;
const bad = val.match(/(b)([0-9])/) && this.badLocation;
if (good || bad) {
this.locations = [...this.locations, val];
} else {
this.locations = [val];
}
}A
s
y
n
c
(resources)
Slow rendering got you down?
Resources to the rescue

Resources
export default class WordFeed extends LifecycleResource {
@cached
get value() {
const { wordList } = this.args.named;
return wordList?.slice(this.offset, this.end) || [];
}
setup() {
const { wordList } = this.args.named;
this.listLength = wordList.length;
}
update() {
clearTimeout(this.timeoutId);
const { displayCount } = this.args.named;
if (this.end < displayCount) {
this.isRunning = true;
this.timeoutId = setTimeout(this.updateEnd, 100);
} else {
clearTimeout(this.timeoutId);
this.timeoutId = null;
this.isRunning = false;
}
}
teardown() {
clearTimeout(this.timeoutId);
}
updateEnd = () => {
const { wordList } = this.args.named;
if (wordList.length === this.listLength && this.hasWords) {
this.end += this.increment;
} else {
this.listLength = wordList.length;
this.end = 0;
}
};
}export default class WordFeed extends LifecycleResource {
@cached
get value() {
const { wordList } = this.args.named;
return wordList?.slice(this.offset, this.end) || [];
}
setup() {
const { wordList } = this.args.named;
this.listLength = wordList.length;
}
update() {
clearTimeout(this.timeoutId);
const { displayCount } = this.args.named;
if (this.end < displayCount) {
this.isRunning = true;
this.timeoutId = setTimeout(this.updateEnd, 100);
} else {
clearTimeout(this.timeoutId);
this.timeoutId = null;
this.isRunning = false;
}
}
teardown() {
clearTimeout(this.timeoutId);
}
updateEnd = () => {
const { wordList } = this.args.named;
if (wordList.length === this.listLength && this.hasWords) {
this.end += this.increment;
} else {
this.listLength = wordList.length;
this.end = 0;
}
};
}Resources
@use wordFeed = WordFeed.with(() => ({
wordList: this.wf.possibleWords,
displayCount: this.displayCount,
increment: 80,
}));thunk!
<div class="word-feed" ...attributes>
{{#if this.wordFeed.value.length}}
{{#each this.wordFeed.value as |word|}}
<Word @api={{this.api}} @word={{word}} />
{{/each}}
{{else}}
<h1>No possible words</h1>
{{/if}}
</div>
export default class WordFeed extends LifecycleResource {
@cached
get value() {
const { wordList } = this.args.named;
return wordList?.slice(this.offset, this.end) || [];
}
setup() {
const { wordList } = this.args.named;
this.listLength = wordList.length;
}
update() {
clearTimeout(this.timeoutId);
const { displayCount } = this.args.named;
if (this.end < displayCount) {
this.isRunning = true;
this.timeoutId = setTimeout(this.updateEnd, 100);
} else {
clearTimeout(this.timeoutId);
this.timeoutId = null;
this.isRunning = false;
}
}
teardown() {
clearTimeout(this.timeoutId);
}
updateEnd = () => {
const { wordList } = this.args.named;
if (wordList.length === this.listLength && this.hasWords) {
this.end += this.increment;
} else {
this.listLength = wordList.length;
this.end = 0;
}
};
}Resources
@use wordFeed = WordFeed.with(() => ({
wordList: this.wf.possibleWords,
displayCount: this.displayCount,
increment: 80,
}));Set it and forget it
before
after
u
n
i
t
e
(services)

Word
- Init method to allow lazy instantiation, also to allow settings service injection
- Accesses word lists
- Builds instances of delegate classes:
- words
- finder
(service)
Finder
- Builds tray instances
- Tracks letter groups
- Provides filtered word lists
(class)
Words
- Builds word list
- Builds letter instances
- Provides unfiltered word lists
(class)
Settings
- Tracks various settings state
- Holds settings specific logic like keyboard layouts
(service)
@service settings;
selectKeyboard = (e) => {
this.settings.keyboard = e.target.value;
};
toggleSetting = (value, update, e) => {
this.settings[value] = e.target.checked;
if (update) this.args.updateSettings();
};
setSelected = modifier((element, [value, selected]) => {
const isSelected = value === selected;
if (isSelected) element.setAttribute('selected', '');
});Settings
- Tracks various settings state
- Holds settings specific logic like keyboard layouts
(service)

<Toggle
{{on "input" (fn this.toggleSetting "autoExclude" true)}}
@id="autoExclude"
@checked={{this.settings.autoExclude}}
>
automatically exclude letters
</Toggle> <Select
value={{this.settings.keyboard}}
@options={{this.settings.keyboardTypeOptions}}
@selected={{this.settings.keyboard}}
@setSelected={{this.setSelected}}
{{on "change" this.selectKeyboard}}
/> <select aria-label="keyboard select"
...attributes>
{{#each @options as |o|}}
<option value={{o.value}} {{@setSelected o.value @selected}} >{{o.name}}</option>
{{/each}}
</select>export default class SettingsService extends Service {
keyboards;
@tracked keyboard = 'qwerty';
@tracked useCommon = true;
@tracked sortAlpha = false;
@tracked autoExclude = true; //false
@tracked selectPlacement = false;<input
type="checkbox"
role="switch"
checked={{@checked}}
id={{concat "switch" @id}} ...attributes>
<label for={{concat "switch" @id}}>{{yield}}</label>It's not technically a solver
It takes no external or historical data into account
You likely won't solve in one or two guesses
The main goal is aiding word discovery
A fun detour
I couldn't decide on a name, which gave me a fun idea.
I followed my interest and spent some time making an easter egg
e
v
e
n
t
(element modifiers)
Modify all the elements
Element modifiers provide an extremely powerful api for connecting functionality to your dom elements in your interface
They can be used for anything from element registry, setup, teardown, attribute manipulation, connecting interfaces, connecting to native apis, element communication, event handling,
Modify all the elements
toggleClass = modifier((el, [eventName, className, classTarget]) => {
const target = classTarget ? document.querySelector(classTarget) : el;
const handler = (e) => {
target.classList.toggle(className);
};
if (!Array.isArray(eventName)) {
eventName = [eventName];
}
for (const ev of eventName) {
el.addEventListener(ev, handler);
}
return () => {
for (const ev of eventName) {
el.removeEventListener(ev, handler);
}
};
});
// template
// eventName = "click", className = "slide-out" classTarget = ".word-container"
<button type="button" {{this.toggleClass "click" "slide-out" ".word-container"}}>view more</button>
<div class="word-container">more info</div>Modify all the elements
setSelected = modifier((element, [value, selected]) => {
const isSelected = value === selected;
if (isSelected) element.setAttribute('selected', '');
});
// template
<select class="button-blue transition ease-in-out" aria-label="keyboard select" ...attributes>
{{#each @options as |o|}}
<option value={{o.value}} {{this.setSelected o.value @selected}} >{{o.name}}</option>
{{/each}}
</select>s
h
a
r
E
(advice)
Tips for creative projects
S
P
e
e
d
(tools and ecosystem)
Speed is critical early on

Ember lightens the load
Addons used
ember-modifier
ember-resources
tracked-built-ins
ember-composable-helpers
ember-concurrency
ember-drag-drop
ember-modal-dialog
and more...
Don't build it
(unless you want to!)
Apply directly to the app
npx ember-apply tailwind
c
l
o
n
e
(starter kit)
A good starting point

Build a Template App with your favorite addons installed!
git clone starter-app
s
t
y
l
e
(appropriately)
Tailwind
or just CSS
Tailwind
or just CSS

Do what you like
Tailwind
(R.I.P. Shock G)
or nothing
Keep it simple
Avoid nested, highly specific selectors
s
a
v
e
s
(git practices)
Commit early, commit often!
Commit as often as you can, even for small changes.
Commit early, commit often!
Commit as often as you can, even for small changes.
// main branch
git checkout main
// define savepoint tag
git tag savepoint
// make changes, commit then tag
git tag experiment-1
// make more changes, commit and tag
git tag experiment-1.2
// reset branch to savepoint and wipe changes
git reset --hard savepoint
// checkout tag in detatched head and start new branch
git checkout experiment-1 -b experiment-1-explorationgit tag {tag}
save point
Commit early, commit often!
Commit as often as you can, even for small changes.
// main branch
git checkout main
// define savepoint tag
git tag savepoint
// make changes, commit then tag
git tag experiment-1
// make more changes, commit and tag
git tag experiment-1.2
// reset branch to savepoint and wipe changes
git reset --hard savepoint
// checkout tag in detatched head and start new branch
git checkout experiment-1 -b experiment-1-explorationgit reset --hard {tag}
load save point
(loses any unsaved data!)
git tag {tag}
save point
Commit early, commit often!
Commit as often as you can, even for small changes.
// main branch
git checkout main
// define savepoint tag
git tag savepoint
// make changes, commit then tag
git tag experiment-1
// make more changes, commit and tag
git tag experiment-1.2
// reset branch to savepoint and wipe changes
git reset --hard savepoint
// checkout tag in detatched head and start new branch
git checkout experiment-1 -b experiment-1-explorationgit checkout {tag} -b {branch}
load save point in new game
git reset --hard {tag}
load save point
(loses any unsaved data!)
git tag {tag}
save point
Commit early, commit often!
Commit as often as you can, even for small changes.
Think of it as save points in a video game
// main branch
git checkout main
// define savepoint tag
git tag savepoint
// make changes, commit then tag
git tag experiment-1
// make more changes, commit and tag
git tag experiment-1.2
// reset branch to savepoint and wipe changes
git reset --hard savepoint
// checkout tag in detatched head and start new branch
git checkout experiment-1 -b experiment-1-explorationgit checkout {tag} -b {branch}
load save point in new game
git reset --hard {tag}
load save point
(loses any unsaved data!)
git tag {tag}
save point


Lose almost all progress but some manual saves
Named saves and can save progress automatically
Progress Loss
Risk Factor
Think of it as save points in a video game


Commit early, commit often!
s
c
o
p
e
(start small)
Starting out small
Nothing slows you down faster than taking on too much
Starting out small
Nothing slows you down faster than taking on too much
Reduce the scope to prevent cognitive overload
Start shallow
Don't go too deep into one aspect and ignore the rest!
Well rounded projects tend to live longer
ux
usable
reliable
functional
ux
usable
reliable
functional
MVP triangle
D
E
A
T
H
(project entropy)
Don't fear the reaper
Every project has a natural lifespan,
influenced by energy and interest
Project graveyards are okay!


Don't fear the reaper
Sometimes they come back!

(prototype)
d
r
a
f
t
Start out small with a simple prototype, or just a function as a proof of concept

Verify in the small
Start out small with a simple prototype, or just a function as a proof of concept
You don't need to have it fully defined before starting, protoyping can help discovery


Verify in the small
(interest)
d
e
p
t
h
Get Serious
(sometimes)
Get Serious
(sometimes)
Follow your interests and see where it takes you
Let your interest drive the level of complexity and effort
Don't build unless you want to
Most Importantly
Run Wild!
Run Wild!
It's a perfect opportunity to overengineer...
Run Wild!
Try out that experimental feature that's too risky for prod...
Run Wild!
Use tabs instead of spaces...
The only limit is your imagination!
jakebixby.com/wordle/
github.com/trabus/wordle-finder
github.com/trabus/wordle-helper-node

auditboard.com/careers/
t
h
a
n
k
s
special thanks to
Jay Gruber, Jen Weber, My Family, and Coworkers for their help!
G
U
I
D
E
Applying Design Thinking
Empathy
Definition
Ideation
Minimum requirements give you direction
but more importantly, they give you freedom to explore.
Keep your data decoupled
If you keep your data decoupled from the UI representation of it, you will find it easier to pivot on a design later.
Array of records
UI display logic
{{#each @items as |item|}}
<Item @api={{this.api}} @data={{item}} />
{{/each}}One way to do this is to use a very limited, generic interface for your arguments.
Build from the outside in
Start with the larger container elements and work down to the smaller detail elements.
Keep things decoupled by using services and interface patterns.
c
r
A
f
t
(good patterns)
Technique matters
Just as in art, technique will help you go further in your endeavors
Technique matters
Just as in art, technique will help you go further in your endeavors




Technique matters




Just as in art, technique will help you go further in your endeavors
Technique matters




Following well defined patterns and making deliberate choices can save a project
c
l
o
c
k
(time and energy)
A good ending point
The Lookup
Included
Excluded
Correct
L
E
a
R
n
The Lookup
L
a
R

With just 3 letters, we have reduced 13,000 words down to 213
The Lookup
L
a
R

included letters help reduce to about 60 possible words
The Lookup
L
a
R

introducing correct letter positions reduce even further, down to 16 possibilities
The Lookup
L
a
R

excluded letters bring us all the way down to 6 possibilities
E
n
all from just one guess
Loses at Wordle Once... Builds Ember App
By Jacob Bixby
Loses at Wordle Once... Builds Ember App
After suffering a humiliating defeat at Wordle for the first time, an engineer ventures on a quest to build an app that will make sure they never feel the sting of losing a Wordle by a single guess, ever again. By leveraging the power of Ember, they were able bring their dream from a simple idea to a fully featured web application, ready to assist hundreds of people at Wordle, all over the world. Join them as they recount their odyssey to build something fun, this talk will take the audience on the personal journey of a software engineer with something to prove (to themself).
- 272