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-exploration
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-exploration
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. 

// 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-exploration
git 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-exploration
git 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

Made with Slides.com