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)
January 18th, 2022
January 18th, 2022
P
R
O
?
?
o
f
G
r
i
e
f
(processing loss)
['shock','denial','anger','bargaining','guilt','depression','acceptance']
.forEach(stage => process(stage));['shock','denial','anger','bargaining','guilt','depression','acceptance']
.forEach(stage => process(stage));I
D
E
A
S
(starting out)
12,974
5,046
12,974
five letter words in the English language (approximately)
five letter words in the English language (approximately)
5,046
12,974
of those are commonly used (not including proper names)
are more commonly used (not including proper names)
five letter words in the English language (approximately)
12,974
5,046
are more commonly used (not including proper names)
five letter words in the English language (approximately)
12,974
5,046
10,657
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
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
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
12,974
12,972
E
M
B
E
R
E
M
B
E
R
EMBER
REACT
EGRET
LEARN
EARTH
EXAMS
BEARS
ENARM
E
M
B
E
R
EMBER
REACT
EGRET
LEARN
EARTH
EXAMS
BEARS
ENARM
E
E
E
E
E
E
M
B
E
R
EMBER
REACT
EGRET
LEARN
EARTH
EXAMS
BEARS
ENARM
M
M
M
E
M
B
E
R
EMBER
REACT
EGRET
LEARN
EARTH
EXAMS
BEARS
ENARM
EXAMS
E
M
B
E
R
EMBER
REACT
EGRET
LEARN
EARTH
BEARS
ENARM
EXAMS
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',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',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',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',m
o
d
e
l
(prototype)
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)
i
n
p
u
t
(data format)
{
correct: {
"0": "e"
},
included: {
"1": ["m"],
},
excluded: [
"b", "e", "r"
]
}E
M
B
E
R
{
correct: {
"0": "e"
},
included: {
"1": ["m"],
},
excluded: [
"b", "e", "r"
]
}E
M
B
E
R
{
correct: {
"0": "e"
},
included: {
"1": ["m"],
},
excluded: [
"b", "e", "r"
]
}E
M
B
E
R
p
r
o
b
e
(word lookup)
E
M
B
E
R
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
Build every possible letter combination for indexing words
E
M
B
E
R
E
M
E
R
M
E
R
M
R
R
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',
]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',
]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',
]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',
]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',
]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',
]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',
]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
Build every possible letter combination for indexing words
a
M
B
E
R
E
M
B
E
R
E
M
B
r
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
Build every possible letter combination for indexing words
a
M
B
E
R
{
'abemr': [
'amber',
'bream',
]
}B
r
e
a
m
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']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)]];
}[
'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)]];
}[
'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)]];
}[
'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',B
U
I
L
D
(user interface)
E
M
B
E
R
(is awesome)
E
M
B
E
R
(is awesome)
T
R
A
C
K
(autotracking)
<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> 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> 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}} /> 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);
};<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';
}<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';
}<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';
}<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';
}<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';
}<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';
} @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)
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;
}
};
} @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;
}
};
} @use wordFeed = WordFeed.with(() => ({
wordList: this.wf.possibleWords,
displayCount: this.displayCount,
increment: 80,
}));u
n
i
t
e
(services)
@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', '');
});<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>e
v
e
n
t
(element modifiers)
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>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)
S
P
e
e
d
(tools and ecosystem)
ember-modifier
ember-resources
tracked-built-ins
ember-composable-helpers
ember-concurrency
ember-drag-drop
ember-modal-dialog
and more...
npx ember-apply tailwind
c
l
o
n
e
(starter kit)
git clone starter-app
s
t
y
l
e
(appropriately)
s
a
v
e
s
(git practices)
// 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
// 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
// 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
// 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
s
c
o
p
e
(start small)
ux
usable
reliable
functional
ux
usable
reliable
functional
MVP triangle
D
E
A
T
H
(project entropy)
(prototype)
d
r
a
f
t
(interest)
d
e
p
t
h
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
Array of records
UI display logic
{{#each @items as |item|}}
<Item @api={{this.api}} @data={{item}} />
{{/each}}c
r
A
f
t
(good patterns)
c
l
o
c
k
(time and energy)
L
E
a
R
n
L
a
R
L
a
R
L
a
R
L
a
R
E
n