Een Wordle maken met TDD in Javascript

Een Wordle maken met TDD in Javascript

We blijven deze geweldige Kata oefenen en leren. Je kunt de stappen volgen!

TL;DR: Javascript is ook geweldig voor TDD

Tijdens de Wordle rush van januari 2022 schreef ik een artikel waarin ik beschreef hoe je een Wordle met TDD kunt maken met behulp van PHP.

Een woordspel ontwikkelen met TDD in 25 minuten

Een paar maanden later transcribeerde ik de UI-versie van een Wordle gemaakt met Codex Artificial Intelligence.

Stap voor stap Wordle-creatie met Codex AI

Ik zal beide werelden combineren om als Centaur te programmeren.

Ik zal ook het proces en de output van verschillende taalversies vergelijken.

Dit is de JavaScript-versie.


Zoals gewoonlijk zullen we ons concentreren op de bedrijfslogica van het spel, wetende dat we de gebruikersinterface kunnen bouwen met natuurlijke taalopdrachten.

In dit artikel zal ik een repl.it gebruiken met Jest.

Javascript heeft veel raamwerken voor het testen van eenheden.

Je kunt gebruiken wat je wilt.

Laten we beginnen…

Volgens dezelfde principes als het vorige artikel, beginnen we met het definiëren van een Wordle Word.

De kleinste informatiehoeveelheid in Wordle is een woord.

We kunnen stellen dat brief is kleiner, maar al het benodigde letterprotocol is al gedefinieerd (we kunnen het mis hebben).

Een woord is geen strijdwagen(5).

Een woord is geen reeks.

Een woord is geen snaar.

Dit is een veel voorkomende fout en een bijectieschending.

HEEFT woord et un snaar hebben verschillende verantwoordelijkheden, ook al kunnen ze elkaar kruisen.

Het vermengen van (per ongeluk) implementatiedetails met (essentieel) gedrag is een wijdverbreide fout.

Dus we moeten definiëren wat is een woord.

Een woord in Wordle is een Geldig 5-letter woord.

Laten we beginnen met ons gelukkige pad:

test("test01ValidWordLettersAreValid", async function() {
  const word = new Word('valid');
  expect(['v', 'a', 'l', 'i', 'd']).toStrictEqual(word.letters());
});

We beweren dat het vragen om letters in ‘geldig’ een array van de letters retourneert.

Dit is het resultaat:

Message: letters from word must be 'valid'
Stack Trace:
ReferenceError: Word is not defined
    at Object. (/home/runner/Wordle-TDD/_test_runnertest_suite.js:6:18)
    at Promise.then.completed (/home/runner/Wordle-TDD/node_modules/jest-circus/build/utils.js:333:28)    

Dit is prima omdat we niet hebben gedefinieerd wat een woord is.

Merk op

  • Dit is een TDD-patroon.
  • We noemen de objecten na hun gedrag zelfs voordat ze bestaan.
  • Woord klasse is nog niet gedefinieerd.
  • De eerste onbeduidende verantwoordelijkheid van ons Woord is om zijn brieven te beantwoorden.
  • Dit is geen doorzetter. Elk woord moet zijn letters beantwoorden.
  • We geven niets om het sorteren van letters. Dat zou een voortijdige optimalisatie en vergulden scenario.
  • We beginnen met een eenvoudig voorbeeld. Geen gedupliceerd.
  • We knoeien nog niet met woordvalidatie (het woord kan XXXXX zijn).
  • We kunnen beginnen met een eenvoudigere test om te valideren dat het woord is gemaakt. Dit zou in strijd zijn met de teststructuur die altijd een bewering vereist.
  • De verwachte waarde moet altijd de eerste in de bewering zijn.

We moeten een woord maken met de brieven() functie.

class Word {
  letters() {
    return ['v', 'a', 'l', 'i', 'd'];
  }  
}

Merk op

  • We hebben (nog) geen constructeurs nodig.
  • We werken hardcode letters omdat dit tot nu toe de eenvoudigst mogelijke oplossing is.
  • Doe alsof tot we het halen.

We voeren alle tests uit (slechts 1) en we zijn in orde.

✅  test01ValidWordLettersAreValid

  All tests have passed 1/1  

Laten we nog een test schrijven:

test("test02FewWordLettersShouldRaiseException", async function() {
  expect(() => { 
    new Word('vali');                 
               }).toThrow(Error);
});

De test mislukt zoals verwacht…

❌  test02FewWordLettersShouldRaiseException
Stack Trace:
Error: expect(received).toThrow(expected)

Expected constructor: Error

Received function did not throw
    at Object.toThrow (/home/runner/Wordle-TDD/_test_runnertest_suite.js:10:23)

✅  test01ValidWordLettersAreValid

  1/2 passed, see errors above  

Merk op

  • De eerste test geslaagd
  • De tweede test zal naar verwachting een uitzondering veroorzaken. Wat het niet deed.
  • We verklaren alleen dat er een generieke uitzondering zal worden gemaakt.
  • We brengen gewoon een generieke fout naar voren.
  • Speciale uitzonderingen maken is een codegeur die naamruimten vervuilt. (tenzij we het opvangen, maar dit gebeurt nu niet).

We moeten onze implementatie veranderen om ervoor te zorgen dat test02 pas (en ook) test01).

class Word {
  constructor(word) {
    if (word.length < 5)
      throw new Error('Too few letters. Should be 5');
  }
  letters() {
      return ['v', 'a', 'l', 'i', 'd'];
  }  
}

En de testen slagen.


✅  test02FewWordLettersShouldRaiseException

✅  test01ValidWordLettersAreValid

  All tests have passed 2/2  

Merk op

  • We gebruiken het constructorargument (nog) niet om de eigenlijke letters in te stellen.
  • We controleren alleen op een paar letters. Niet voor al te veel aangezien we nog geen dekkingstoets hebben.
  • TDD vereist volledige dekking. Het toevoegen van nog een controle zonder een test is een technische overtreding.

Laten we controleren op te veel.

test("test03TooManyWordLettersShouldRaiseException", async function() {
  expect(() => { 
    new Word('toolong');                 
               }).toThrow(Error);

});

We voeren ze uit:

❌  test03TooManyWordLettersShouldRaiseException
Stack Trace:
Error: expect(received).toThrow(expected)

Expected constructor: Error

Received function did not throw
    at Object.toThrow (/home/runner/Wordle-TDD/_test_runnertest_suite.js:10:23)

✅  test02FewWordLettersShouldRaiseException

✅  test01ValidWordLettersAreValid

  2/3 passed, see errors above  

We voegen de validatie toe:

class Word {
  constructor(letters) {
    if (letters.length < 5)
      throw new Error('Too few letters. Should be 5');
    if (letters.length > 5)
      throw new Error('Too many letters. Should be 5');
  }
  letters() {
      return ['v', 'a', 'l', 'i', 'd'];
  }  
}

En alle testen zijn geslaagd.

All tests have passed 3/3  

We kunnen nu een (optionele) refactor maken en de functie wijzigen om een ​​bereik te bevestigen in plaats van twee grenzen. We besluiten het zo te laten omdat het meer declaratief is.

We kunnen ook een test voor nulwoorden toevoegen volgens de Zombie-methodologie.

Laten we het doen.

test("test04EmptyLettersShouldRaiseException", async function() {
  expect(() => { 
    new Word('');                 
               }).toThrow(Error);

});
✅  test04EmptyLettersShouldRaiseException

✅  test03TooManyWordLettersShouldRaiseException

✅  test02FewWordLettersShouldRaiseException

✅  test01ValidWordLettersAreValid

Het is geen verrassing dat de test slaagt, aangezien we al een test hebben voor dit scenario.

Aangezien deze test geen waarde toevoegt, moeten we deze verwijderen.


Laten we nu eens kijken wat geldige letters zijn:

test("test05InvalidLettersShouldRaiseException", async function() {
   expect(() => { 
    new Word('vali*');                 
               }).toThrow(Error);

});

... en de test is verbroken omdat er geen bewering wordt gedaan.


❌  test05InvalidLettersShouldRaiseException
Stack Trace:
Error: expect(received).toThrow(expected)

Expected constructor: Error

Received function did not throw

We moeten de code corrigeren...

class Word {
  constructor(word) {
    if (word.length < 5)
      throw new Error('Too few letters. Should be 5');
    if (word.length > 5)
      throw new Error('Too many letters. Should be 5');
    if (word.indexOf('*') > -1) 
      throw new Error('Word has invalid letters');
  }
}

En alle tests slagen omdat we duidelijk hardcoderen.

All tests have passed 5/5  

Merk op

  • We hebben de asterisk hard gecodeerd als het enige ongeldige teken (voor zover we weten).
  • We kunnen de controlecode plaatsen: voordat goud na de vorige validaties. -- Totdat we een ongeldig geval hebben (met ongeldige tekens en ongeldige lengte) kunnen we het verwachte gedrag niet aannemen.

Laten we meer ongeldige letters toevoegen en de code corrigeren.

test("test06PointShouldRaiseException", async function() {
   expect(() => { 
    new Word('val.d');                 
               }).toThrow(Error);

});

// Solution

 constructor(word) {
    if (word.indexOf('*') > -1) 
      throw new Error('Word has invalid letters');
    if (word.indexOf('.') > -1) 
      throw new Error('Word has invalid letters');
}

Merk op

  • We hebben (nog) geen meer generieke functie geschreven omdat we tests en refactor niet tegelijkertijd kunnen corrigeren (de techniek verbiedt ons).

Alle testen zijn in orde.

We kunnen refactoren.

We vervangen de laatste twee zinnen.

class Word {
  constructor(word) {
    if (word.length < 5)
      throw new Error('Too few letters. Should be 5');
    if (word.length > 5)
      throw new Error('Too many letters. Should be 5');
    // Refactor  
    if (!word.match(/^[a-z]+$/i)) 
      throw new Error('word has invalid letters');
    //   
}

Merk op

  • We kunnen alleen refactoren als we de tests niet tegelijkertijd wijzigen.
  • De bewering controleert alleen op hoofdletters. Aangezien we tot nu toe met deze voorbeelden te maken hebben.
  • Ontwerpbeslissingen stellen we zoveel mogelijk uit.
  • We hebben een reguliere expressie gedefinieerd op basis van Engelse letters. We zijn er vrij zeker van dat het geen Spaans (ñ), Duits (ë), enz. zal accepteren.

Als controlepunt hebben we voortaan nog maar vijfletterwoorden.

Laten we beweren brieven() functie.

We hebben het hard gecodeerd.

TDD Opent vele paden.

We moeten ze allemaal bijhouden totdat we nieuwe openen.

We moeten woorden vergelijken.

test("test07TwoWordsAreNotTheSame", async function() {
    const firstWord = new Word('valid');
    const secondWord = new Word('happy');
    expect(firstWord).not.toStrictEqual(secondWord);
});

test("test08TwoWordsAreTheSame", async function() {
    const firstWord = new Word('valid');
    const secondWord = new Word('valid');
    expect(firstWord).toStrictEqual(secondWord);
});

En de test mislukt.

Laten we de parameter gebruiken die we naar hen sturen.

class Word {
  constructor(word) { 
    // ...
    this._word = word;
  }
  letters() {
      return ['v', 'a', 'l', 'i', 'd'];
  }  
}
✅  test08TwoWordsAreTheSame

✅  test07TwoWordsAreNotTheSame

✅  test06PointShouldRaiseException

✅  test05InvalidLettersShouldRaiseException

✅  test04EmptyLettersShouldRaiseException

✅  test03TooManyWordLettersShouldRaiseException

✅  test02FewWordLettersShouldRaiseException

✅  test01ValidWordLettersAreValid

  All tests have passed 8/8  

Merk op

  • We slaan de letters op en dit is voldoende voor objectvergelijking (het kan van de taal afhangen).
  • letters() functie is nog steeds hardcoded

We voegen een ander woord toe voor het vergelijken van letters.

Herinneren brieven() functie was tot nu toe hardcoded.

test("test09LettersForGrassWord", async function() {
  const grassWord = new Word('grass'); 
  expect(['g','r','a','s','s']).toStrictEqual(grassWord.letters());
});

En de test mislukt zoals verwacht.

❌  test09LettersForGrassWord
Stack Trace:
Error: expect(received).toStrictEqual(expected) // deep equality

- Expected  - 4
+ Received  + 4

  Array [
-   "v",
+   "g",
+   "r",
    "a",
-   "l",
-   "i",
-   "d",
+   "s",
+   "s",
  ]
    at Object.toStrictEqual (/home/runner/Wordle-TDD/_test_runnertest_suite.js:9:37)

Merk op

  • Het is erg belangrijk om te controleren op gelijkheid/ongelijkheid in plaats van assertTrue() aangezien veel IDE's een vergelijkingstool openen op basis van de objecten.
  • Dit is nog een reden om IDE's te gebruiken en nooit teksteditors.

Laten we de veranderen brieven() functioneren sinds we het vervalsen.

class Word {
  letters() {
      return this._word.split("");
  }  
}

We moeten ervoor zorgen dat vergelijkingen niet hoofdlettergevoelig zijn.

test("test10ComparisonIsCaseInsensitve", async function() {
    const firstWord = new Word('valid');
    const secondWord = new Word('VALID');
    expect(firstWord).toStrictEqual(secondWord); 
});

Test mislukt.

We moeten een beslissing nemen.

We besluiten dat al onze domeinen kleine letters zullen zijn.

We staan ​​geen hoofdletters toe, ondanks dat de gebruikersinterface hoofdletters heeft.

We zullen geen magische conversies doen.

We veranderen de test om ongeldige hoofdletters op te vangen en te corrigeren.

test("test10NoUppercaseAreAllowed", async function() {
   expect(() => { 
    new Word('vAliD');                 
               }).toThrow(Error);
});

class Word {
  constructor(word) {
    // We remove the /i modifier on the regular expression  
    if (!word.match(/^[a-z]+$/)) 
      throw new Error('word has invalid letters');   
  }

Onze woorden staan ​​in een bijectie met Engelse Wordle-woorden. of niet?

Laten we een niet-Engels woord proberen.

test("test11XXXXIsnotAValidWord", async function() {
  expect(() => { 
    new Word('XXXXX');                 
               }).toThrow(Error);
});

Deze test mislukt.

We vangen geen ongeldige Engelse 5-letterwoorden op.

Merk op

  • We moeten een beslissing nemen. Volgens onze bijectie is er een extern woordenboek dat geldige woorden beweert.
  • We kunnen valideren met het woordenboek bij het maken van woorden. Maar we willen dat het woordenboek geldige woorden opslaat. Geen strings.
  • Het is een ei-kip probleem.
  • We besluiten om te gaan met ongeldige woorden in het woordenboek en niet met het woord Wordle.
  • We verwijderen de toets.
  • We zullen binnen enkele ogenblikken een betere manier vinden.

Laten we het spel maken.

We beginnen te praten over een spel dat niet bestaat.

test("test11EmptyGameHasNoWinner", async function() {
  const game = new Game()
  expect(false).toStrictEqual(game.hasWon());
});

Test mislukt.

We moeten de klasse en de functie maken.

class Game {
  hasWon() {
      return false;
  }  
}

We implementeren geprobeerde woorden.

En de eenvoudigste oplossing.

Hardcoderen zoals altijd.

test("test12EmptyGameWordsAttempted", async function() {
  const game = new Game()
  expect([]).toStrictEqual(game.wordsAttempted());
});

class Game {
  wordsAttempted() {
    return [];
  }
}

✅  test12EmptyGameWordsAttempted
...
  All tests have passed 12/12  
test("test13TryOneWordAndRecordIt", async function() {
  var game = new Game();
  game.addAttempt(new Word('loser'));
  expect([new Word('loser')]).toStrictEqual(game.wordsAttempted());   
});

class Game {
  constructor() {
    this._attempts = [];
  }
  hasWon() {
      return false;
  }
  wordsAttempted() {
    return this._attempts;
  }
  addAttempt(word) {
    this._attempts.push(word);    
  }
}

Merk op

  • We slaan de pogingen lokaal op en voegen de poging toe en veranderen ook de echte implementatie van woordenAttempted().

We kunnen hasLost() implementeren als het 6 pogingen mist.

Met de eenvoudigste implementatie zoals gewoonlijk.

test("test14TryOneWordAndDontLooseYet", async function() {
  const game = new Game();
  game.addAttempt(new Word('loser'));
  expect(false).toStrictEqual(game.hasLost());   
});

class Game { 
  hasLost() {
      return false;
  }
}

Merk op

  • We leren de regels terwijl ons model groeit.

Zoals gewoonlijk. We stoppen met faken en besluiten het te maken.

test("test15TryFiveWordsLoses", async function() {
  const game = new Game([new Word('loser'), new Word('music')], new Word('music'));
  game.addAttempt(new Word('loser'));
  game.addAttempt(new Word('loser'));
  game.addAttempt(new Word('loser'));
  game.addAttempt(new Word('loser'));
  game.addAttempt(new Word('loser'));
  expect(false).toStrictEqual(game.hasLost());  
  // last attempt
  game.addAttempt(new Word('loser'));
  expect(true).toStrictEqual(game.hasLost());  
});

class Game {
  hasLost() {
    return this._attempts.length > 5;
  }
}

We hebben de meeste mechanica.

Laten we een geldig woordenwoordenboek toevoegen en ongeldig spelen.

test("test16TryToPlayInvalid", async function() {
  const game = new Game([]);  
  expect(() => { 
    game.addAttempt(new Word('xxxxx'));            
               }).toThrow(Error);
});

De test mislukt zoals verwacht.

Wij repareren het.

class Game {
  constructor(validWords) {
    this._attempts = [];
    this._validWords = validWords;
  }   
  addAttempt(word) {
    if (!this._validWords.some(validWord => validWord.sameAs(word))) {
      throw new Error(word.letters() + " is not a valid word");
    }
    this._attempts.push(word);    
  }
}

// fix previous tests
// change 

const game = new Game([]);

// to 

const game = new Game([new Word('loser')]);

Also add: 
Class Word {
 sameAs(word) {
    return word.word() == this.word();
  }
}

en de test staat vast, maar...

  test16TryToPlayInvalid

❌  test15TryFiveWordsLoses
Stack Trace:
TypeError: Cannot read properties of undefined (reading 'includes')

❌  test14TryOneWordAndDontLooseYet
Stack Trace:
TypeError: Cannot read properties of undefined (reading 'includes') 

❌  test13TryOneWordAndRecordIt
Stack Trace:
TypeError: Cannot read properties of undefined (reading 'includes')

✅  test12EmptyGameWordsAttempted

✅  test10EmptyGameHasNoWinner

  12/15 passed, see errors above  

Merk op

  • test13, test14 en test15 werkten voorheen.
  • Nu zijn ze verbroken sinds we een nieuwe bedrijfsregel hebben toegevoegd.
  • We moeten het woordenboek doorgeven bij het maken van het spel.
  • We lossen de drie op door een array toe te voegen met de woorden die we zullen gebruiken.
  • Het is een goed teken dat onze setup complex wordt om geldige scenario's te blijven maken.

Nu spelen we om te winnen.

We voegen de test toe en moeten hasWon() dienovereenkomstig wijzigen.

test("test17GuessesWord", async function() {
  const words = [new Word('happy')];
  const correctWord = new Word('happy');
  const game = new Game(words, correctWord);  
  expect(game.hasWon()).toStrictEqual(false);
  game.addAttempt(new Word('happy'));
  expect(game.hasWon()).toStrictEqual(true);
});

// we need to store the correct word
class Game {
  constructor(validWords, correctWord) {
    this._attempts = [];
    this._validWords = validWords;
    this._correctWord = correctWord;
  }
  hasWon() {
    return this._attempts.some(attempt => attempt.sameAs(this._correctWord)); 
}

Merk op

  • We gebruiken geen vlaggen om te controleren of iemand heeft gewonnen. We kunnen het direct controleren.
  • Het maakt ons niet uit of het bij een eerdere poging heeft gewonnen.
  • We maken een addParameter refactor met dit nieuwe element naar eerdere speldefinities.

We hebben de . toegevoegd Correct woord.

We moeten bevestigen dat dit woord in het woordenboek staat.

test("test18CorrectWordNotInDictionary", async function() {
  const words = [new Word('happy')];
  const correctWord = new Word('heros');  
   expect(() => { 
     new Game(words, correctWord);                 
               }).toThrow(Error);
});

class Game {
  constructor(validWords, correctWord) {
    if (!validWords.some(validWord => validWord.sameAs(correctWord)))
      throw new Error("Correct word " + word.word() + " is not a valid word");  
  }

Merk op

  • We moesten alle vorige games veranderen, omdat we voor de start het winnaarsspel moesten halen
  • Dit is een goede bijwerking omdat het de voorkeur geeft aan complete en onveranderlijke objecten.

✅  test18CorrectWordNotInDictionary
...

✅  test01ValidWordLettersAreValid

  All tests have passed 17/17  

Wat gebeurt er als we winnen in de laatste poging?

Zombies vragen ons altijd om te controleren op (B)grenzen waar bugs zich verbergen.

test("test19TryFiveWordsWins", async function() {
  const game = new Game([new Word('loser'),new Word('heros')],new Word('heros'));
  game.addAttempt(new Word('loser'));
  game.addAttempt(new Word('loser'));
  game.addAttempt(new Word('loser'));
  game.addAttempt(new Word('loser'));
  game.addAttempt(new Word('loser'));
  expect(false).toStrictEqual(game.hasLost());  
  expect(false).toStrictEqual(game.hasWon());  
  // last attempt
  game.addAttempt(new Word('heros'));
  expect(false).toStrictEqual(game.hasLost());  
  expect(true).toStrictEqual(game.hasWon());  
});

// And the correction

hasLost() {
    return !this.hasWon() && this._attempts.length > 5;
  }

We hebben alle mechanica.

Laten we de posities van de letter toevoegen.

We kunnen het doen in de Word-klasse.

test("test20LettersDoNotMatch", async function() {
  const firstWord = new Word('trees');
  const secondWord = new Word('valid');
  expect([]).toStrictEqual(firstWord.matchesPositionWith(secondWord));
});

Zoals gewoonlijk krijgen we een ongedefinieerde functie fout:

❌  test20LettersDoNotMatch
Stack Trace:
TypeError: firstWord.matchesPositionWith is not a function

Laten we het zoals gewoonlijk doen alsof.

class Word {
  matchesPositionWith(correctWord) {
    return [];    
  }
}

Merk op

  • Namen zijn altijd erg belangrijk.
  • We kunnen de parameter een naam geven: nog een woord.
  • We geven er de voorkeur aan correct woord.
  • We zijn ons ervan bewust dat we binnenkort een ingewikkeld algoritme nodig hebben en dat rollen duidelijk en contextueel moeten zijn.

laten we matchen

test("test21MatchesFirstLetter", async function() {
  const guessWord = new Word('trees');
  const correctWord = new Word('table');
  expect([1]).toStrictEqual(guessWord.matchesPositionWith(correctWord));
});

mislukt.

We moeten het beter definiëren

Dit is een goed genoeg algoritme.

Lelijk en imperatief

We zullen het later zeker refactoren.

matchesPositionWith(correctWord) {
   var positions = [];
   for (var currentPosition = 0; 
      currentPosition < this.letters().length; 
      currentPosition++) {
       if (this.letters()[currentPosition] == correctWord.letters()[currentPosition]) {
             positions.push(currentPosition + 1); 
             //Humans start counting on 1
       }
   }
   return positions;
}

En alle tests slagen.

Merk op

  • De overeenkomende eigenschap is niet symmetrisch

Nu hebben we de laatste stappen nodig.

Matching in verkeerde posities.

en altijd de eenvoudigste oplossing...

test("test23MatchesIncorrectPositions", async function() {
  const guessWord = new Word('trees');
  const correctWord = new Word('drama');
  expect([2]).toStrictEqual(guessWord.matchesPositionWith(correctWord));
  expect([]).toStrictEqual(guessWord.matchesIncorrectPositionWith(correctWord));
});

// The simplest solution

class Word {
  matchesIncorrectPositionWith(correctWord) {
     return [];
  }
}

Merk op

  • Door deze veilige, nulgevallen toe te voegen, missen we veel gebruikelijke bugs.

Een pittiger testgeval.

test("test24MatchesIncorrectPositionsWithMatch", async function() {
  const guessWord = new Word('alarm');
  const correctWord = new Word('drama');
  expect([3]).toStrictEqual(guessWord.matchesPositionWith(correctWord));
  expect([1, 4, 5]).toStrictEqual(guessWord.matchesIncorrectPositionWith(correctWord));
  // A*ARM vs *RAMA
  expect([3]).toStrictEqual(correctWord.matchesPositionWith(guessWord));
  expect([2, 4, 5]).toStrictEqual(correctWord.matchesIncorrectPositionWith(guessWord));
});

Laten we gaan voor de implementatie

 class Word {
  matchesIncorrectPositionWith(correctWord) {
      var positions = [];
      for (var currentPosition = 0; currentPosition < 5; currentPosition++) {
        if (correctWord.letters().includes(this.letters()[currentPosition])) {
          positions.push(currentPosition + 1);
        }
      }
      return positions.filter(function(position) {
        return !this.matchesPositionWith(correctWord).includes(position);
     }.bind(this));
    }
  }
}

Dat is het.

We hebben een heel klein model geïmplementeerd met alle zinvolle regels.

All tests have passed 21/21  
test("test20220911", async function() {
  const correctWord = new Word('tibia');
    // Sorry for the spoiler
  const words = [
    // all the words I've tried
    new Word('paper'), 
    new Word('tools'),
    new Word('music'),
    new Word('think'), 
    new Word('twins'),
    new Word('tight'),
    // plus the winning word
    correctWord
  ];
  
  const game = new Game(words, correctWord);  
  expect(game.hasWon()).toStrictEqual(false);
  expect(game.hasLost()).toStrictEqual(false);
  // P(A)PER vs TIBIA
  game.addAttempt(new Word('paper'));
  expect([]).toStrictEqual((new Word('paper')).matchesPositionWith(correctWord));
  expect([2]).toStrictEqual((new Word('paper')).matchesIncorrectPositionWith(correctWord));
  // [T]OOLS vs TIBIA
  expect([1]).toStrictEqual((new Word('tools')).matchesPositionWith(correctWord));
  expect([]).toStrictEqual((new Word('tools')).matchesIncorrectPositionWith(correctWord));  
  game.addAttempt(new Word('tools'));
  // MUS[I]C vs TIBIA
  expect([4]).toStrictEqual((new Word('music')).matchesPositionWith(correctWord));
  expect([]).toStrictEqual((new Word('music')).matchesIncorrectPositionWith(correctWord));
  game.addAttempt(new Word('music'));
  // [T]H(I)NK vs TIBIA
  expect([1]).toStrictEqual((new Word('think')).matchesPositionWith(correctWord));
  expect([3]).toStrictEqual((new Word('think')).matchesIncorrectPositionWith(correctWord));
  game.addAttempt(new Word('think'));
  // [T]W(I)NS vs TIBIA
  expect([1]).toStrictEqual((new Word('twins')).matchesPositionWith(correctWord));
  expect([3]).toStrictEqual((new Word('twins')).matchesIncorrectPositionWith(correctWord));  
  game.addAttempt(new Word('twins'));  
  expect(game.hasWon()).toStrictEqual(false);
  expect(game.hasLost()).toStrictEqual(false);
  // [T][I]GHT vs TIBIA
  expect([1, 2]).toStrictEqual((new Word('tight')).matchesPositionWith(correctWord));
  expect([]).toStrictEqual((new Word('tight')).matchesIncorrectPositionWith(correctWord));  
  
  game.addAttempt(new Word('tight'));
  expect(game.hasWon()).toStrictEqual(false);
  expect(game.hasLost()).toStrictEqual(true);
});

(Je vindt meer dagelijkse voorbeelden in de repo)

Ik was erg blij met mijn werkende wordle.

Toen las ik over de complexe regels ervan

Nieuwe regels leren is geen probleem als we TDD hebben.

Laten we de voorbeelden uit het artikel bespreken

test("test25VeryComplexWrongPositions", async function() {

  const guessWord = new Word('geese');
  const correctWord = new Word('those');
  expect([4, 5]).toStrictEqual(guessWord.matchesPositionWith(correctWord));
  expect(['s','e']).toStrictEqual(guessWord.lettersAtCorrectPosition(correctWord));
  expect([]).toStrictEqual(guessWord.lettersAtWrongtPosition(correctWord));
  expect([]).toStrictEqual(guessWord.matchesIncorrectPositionWith(correctWord));
  // GEE[S][E] vs THOSE

  const anotherGuessWord = new Word('added');
  const anotherCorrectWord = new Word('dread');
  expect([5]).toStrictEqual(anotherGuessWord.matchesPositionWith(anotherCorrectWord));
  expect(['d']).toStrictEqual(anotherGuessWord.lettersAtCorrectPosition(anotherCorrectWord));
  expect(['a', 'd', 'e']).toStrictEqual(anotherGuessWord.lettersAtWrongtPosition(anotherCorrectWord));
  expect([1, 2, 4]).toStrictEqual(anotherGuessWord.matchesIncorrectPositionWith(anotherCorrectWord));
  // (A)(D)D(E)[D] vs DREAD
  
  const yetAnotherGuessWord = new Word('mamma');
  const yetAnotherCorrectWord = new Word('maxim');
  expect([1, 2]).toStrictEqual(yetAnotherGuessWord.matchesPositionWith(yetAnotherCorrectWord));
  expect(['m', 'a']).toStrictEqual(yetAnotherGuessWord.lettersInCorrectPosition(yetAnotherCorrectWord));
  expect(['m']).toStrictEqual(yetAnotherGuessWord.lettersAtWrongtPosition(yetAnotherCorrectWord));
  expect([3]).toStrictEqual(yetAnotherGuessWord.matchesIncorrectPositionWith(yetAnotherCorrectWord));
  // [M][A](M)MA vs MAXIM
});

Laten we het algoritme uit het artikel stelen.

matchesIncorrectPositionWith(correctWord) {     
    const correctPositions = this.matchesPositionWith(correctWord);
    var incorrectPositions = [];
    var correctWordLetters = correctWord.letters();
    var ownWordLetters = this.letters();
    for (var currentPosition = 0; currentPosition < 5; currentPosition++) {
      if (correctPositions.includes(currentPosition + 1)) {
        // We can use these wildcards since they are no valid letters
        correctWordLetters.splice(currentPosition, 1, '*');
        ownWordLetters.splice(currentPosition, 1, '+');
      }
    }    
    for (var currentPosition = 0; currentPosition < 5; currentPosition++) {
      const positionInCorrectWord = correctWordLetters.indexOf(ownWordLetters[currentPosition]);
      if (positionInCorrectWord != -1) {        
        correctWordLetters.splice(positionInCorrectWord, 1, '*');
        ownWordLetters.splice(currentPosition, 1, '+');
        incorrectPositions.push(currentPosition + 1); 
      }
    }    
    return incorrectPositions;
  }

We moeten nog een functie toevoegen (wat handig is voor toetsenbordkleuren).

lettersAtCorrectPosition(correctWord) {
    return this.matchesPositionWith(correctWord).map(position => this.letters()[position -1 ]);
}
  
lettersAtWrongtPosition(correctWord) {
    return this.matchesIncorrectPositionWith(correctWord).map(position => this.letters()[position -1]);
}

Merk op

  • Het algoritme verandert een kopie van het juiste woord door '*' te plaatsen wanneer de juiste positie overeenkomt
  • Het verbergt ook de bezochte letters door te veranderen in een speciale (een ongeldige '+').
DREAD vs ADDED
DREA* vs ADDE+
DRE** vs +DDE+
*RE** vs ++DE+
*R*** vs ++D++

Deze oplossing is anders en completer dan de vorige.

De wordle regels zijn niet veranderd.

Volgens David Farley moeten we experts zijn in leren.

En we leren door het beoefenen van kata's zoals deze.

We eindigen met 2 compacte klassen waarin we ons bedrijfsmodel hebben gedefinieerd.

Dit kleine model heeft een echte 1:1 bijectie in de MAPPER naar de echte wereld.

Het is klaar om te evolueren.

Deze game is een metafoor voor echte software-engineering.

Ik hoop dat je het interessant vindt en de kata met mij volgt.

Je kunt spelen met de werkende repl.it.

  • Combineer deze oplossing met de AI-Generated
  • Gebruik een echt woordenboek
  • De taal en het alfabet wijzigen
  • Verander de regels in een ander woord

Ook hier gepubliceerd

BEZIG MET LADEN
. . . opmerkingen & meer!

Leave a Reply

Your email address will not be published.