Créer un Wordle avec TDD en Javascript

Nous continuons à pratiquer cet incroyable Kata et à apprendre. Vous pouvez suivre les étapes !

TL; DR : Javascript est également génial pour TDD

Lors de la ruée vers Wordle de janvier 2022, j’ai écrit un article décrivant comment créer un Wordle avec TDD en utilisant PHP.

Comment développer un jeu Wordle en utilisant TDD en 25 minutes

Quelques mois après, j’ai transcrit la version UI d’un Wordle créé avec Codex Artificial Intelligence.

Création de mots étape par étape avec Codex AI

Je combinerai les deux mondes pour programmer en tant que centaure.

Je comparerai également le processus et le résultat de différentes versions linguistiques.

Ceci est la version Javascript.


Comme d’habitude, nous nous concentrerons sur la logique métier du jeu, sachant que nous pouvons construire l’interface utilisateur avec des commandes en langage naturel.

Dans cet article, je vais utiliser un repl.it avec Jest.

Javascript a de nombreux frameworks de tests unitaires.

Vous pouvez utiliser ce que vous voulez.

Commençons…

Suivant les mêmes principes que l’article précédent, nous commencerons par définir un Wordle Word.

La plus petite quantité d’informations dans un mot est un mot.

Nous pouvons soutenir que lettre est plus petit, mais tout le protocole de lettre nécessaire est déjà défini (nous pouvons nous tromper).

Un mot n’est pas un caractère(5).

Un mot n’est pas un déployer.

Un mot n’est pas un chaîne de caractères.

C’est une erreur courante et un acte répréhensible.

UN mot et un chaîne de caractères ont des responsabilités différentes, bien qu’elles puissent se recouper.

Mélanger des détails d’implémentation (accidentels) avec un comportement (essentiel) est une erreur répandue.

Nous devons donc définir qu’est-ce qu’un mot.

Un mot dans Word est un valide Mot de 5 lettres.

Commençons par notre chemin heureux :

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

Nous affirmons que demander des lettres dans ‘valid’ renvoie un tableau de lettres.

Voici le résultat :

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)    

C’est bien puisque nous n’avons pas défini ce qu’est un mot.

Remarquer

  • Il s’agit d’un modèle TDD.
  • Nous nommons les objets en fonction de leur comportement avant même qu’ils n’existent.
  • Mot la classe n’est pas encore définie.
  • La première responsabilité triviale de notre Parole est de répondre à ses lettres.
  • Ce n’est pas un getter. Chaque mot wordle doit répondre à ses lettres.
  • Nous ne nous soucions pas du tri des lettres. Ce serait un optimisation prématurée et scénario de placage à l’or.
  • Nous commençons par un exemple simple. Pas de doublon.
  • Nous ne plaisantons pas encore avec la validation des mots (le mot pourrait être XXXXX).
  • On peut commencer par un test plus simple validant que le mot est créé. Cela violerait la structure de test qui nécessite toujours une assertion.
  • La valeur attendue doit toujours être la première dans l’assertion.

Nous devons créer un mot avec le des lettres() fonction.

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

Remarquer

  • Nous n’avons pas (encore) besoin de constructeurs.
  • Nous codifions en dur les lettres car c’est la solution la plus simple possible jusqu’à présent.
  • Faire semblant jusqu’à ce que nous le fassions.

Nous exécutons tous les tests (seulement 1) et tout va bien.

✅  test01ValidWordLettersAreValid

  All tests have passed 1/1  

Écrivons un autre test :

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

Le test échoue comme prévu…

❌  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  

Remarquer

  • Le premier test réussi
  • Le deuxième test devrait lever une exception. Ce qui n’a pas été le cas.
  • Nous déclarons juste une exception générique qui sera levée.
  • Nous soulevons juste une erreur générique.
  • La création d’exceptions spéciales est une odeur de code qui pollue les espaces de noms. (sauf si nous l’attrapons, mais cela ne se produit pas en ce moment).

Nous devons changer notre implémentation pour faire test02 passer (et aussi 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'];
  }  
}

Et les tests passent.


✅  test02FewWordLettersShouldRaiseException

✅  test01ValidWordLettersAreValid

  All tests have passed 2/2  

Remarquer

  • Nous n'utilisons pas (encore) l'argument constructeur pour configurer les lettres réelles.
  • Nous vérifions juste quelques lettres. Pas pour trop de monde puisque nous n'avons pas encore de test de couverture.
  • TDD nécessite une couverture complète. Ajouter une autre vérification sans test est une violation de la technique.

Vérifions trop.

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

});

Nous les gérons :

❌  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  

Nous ajoutons la validation :

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'];
  }  
}

Et tous les tests ont réussi.

All tests have passed 3/3  

Nous pouvons maintenant créer un refactor (facultatif) et modifier la fonction pour affirmer une plage au lieu de deux limites. Nous décidons de le laisser ainsi car il est plus déclaratif.

Nous pouvons également ajouter un test pour les mots zéro suivant la méthodologie Zombie.

Faisons-le.

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

});
✅  test04EmptyLettersShouldRaiseException

✅  test03TooManyWordLettersShouldRaiseException

✅  test02FewWordLettersShouldRaiseException

✅  test01ValidWordLettersAreValid

Il n'est pas surprenant que le test réussisse puisque nous avons déjà un test couvrant ce scénario.

Comme ce test n'ajoute aucune valeur, nous devrions le supprimer.


Vérifions maintenant quelles sont les lettres valides :

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

});

...et le test est cassé puisqu'aucune assertion n'est levée.


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

Expected constructor: Error

Received function did not throw

Il faut corriger le code...

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');
  }
}

Et tous les tests réussissent puisque nous sommes clairement en train de coder en dur.

All tests have passed 5/5  

Remarquer

  • Nous codons en dur l'astérisme pour qu'il soit le seul caractère invalide (à notre connaissance).
  • Nous pouvons placer le code de vérification avant de ou après les validations précédentes. -- Jusqu'à ce que nous ayons une casse invalide (avec des caractères invalides et une longueur invalide), nous ne pouvons pas supposer le comportement attendu.

Ajoutons plus de lettres invalides et corrigeons le code.

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');
}

Remarquer

  • Nous n'avons pas (encore) écrit de fonction plus générique car nous ne pouvons pas corriger les tests et refactoriser en même temps (la technique nous l'interdit).

Tous les tests sont ok.

Nous pouvons refactoriser.

Nous remplaçons les deux dernières phrases.

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');
    //   
}

Remarquer

  • Nous ne pouvons refactoriser que si nous ne modifions pas les tests en même temps.
  • L'assertion vérifie uniquement les lettres majuscules. Puisque nous avons affaire à ces exemples jusqu'à présent.
  • Nous reportons autant que possible les décisions de conception.
  • Nous avons défini une expression régulière basée sur les lettres anglaises. Nous sommes à peu près sûrs qu'il n'acceptera pas l'espagnol (ñ), l'allemand (ë), etc.

En tant que point de contrôle, nous n'avons plus que des mots de cinq lettres à partir de maintenant.

Affirmons sur des lettres() fonction.

Nous l'avons laissé codé en dur.

TDD Ouvre de nombreuses voies.

Nous devons garder une trace de chacun d'eux jusqu'à ce que nous en ouvrions de nouveaux.

Il faut comparer les mots.

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);
});

Et le test échoue.

Utilisons le paramètre que nous leur envoyons.

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  

Remarquer

  • Nous stockons les lettres et cela suffit pour la comparaison d'objets (cela peut dépendre de la langue).
  • la fonction lettres() est toujours codée en dur

Nous ajoutons un mot différent pour la comparaison des lettres.

Rappelles toi des lettres() fonction était codée en dur jusqu'à présent.

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

Et le test échoue comme prévu.

❌  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)

Remarquer

  • Il est très important de vérifier l'égalité/inégalité au lieu de assertTrue() car de nombreux IDE ouvrent un outil de comparaison basé sur les objets.
  • C'est une autre raison d'utiliser des IDE et jamais des éditeurs de texte.

Changeons le des lettres() fonction depuis que nous avons fait semblant.

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

Nous devons nous assurer que les comparaisons ne sont pas sensibles à la casse.

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

Le test échoue.

Nous devons prendre une décision.

Nous décidons que tous nos domaines seront en minuscules.

Nous n'autoriserons pas les lettres majuscules bien que l'interface utilisateur ait des majuscules.

Nous ne ferons pas de conversions magiques.

Nous modifions le test pour détecter les lettres majuscules invalides et les corriger.

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');   
  }

Nos mots sont en contradiction avec les mots anglais Wordle. ou non?

Essayons un mot non anglais.

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

Ce test échoue.

Nous n'attrapons pas les mots anglais invalides de 5 lettres.

Remarquer

  • Nous devons prendre une décision. Selon notre bijection, il existe un dictionnaire externe affirmant des mots valides.
  • Nous pouvons valider avec le dictionnaire lors de la création du mot. Mais nous voulons que le dictionnaire stocke des mots valides. Pas de cordes.
  • C'est un problème œuf-poulet.
  • Nous décidons de traiter les mots invalides dans le dictionnaire et non le mot Wordle.
  • Nous supprimons le test.
  • Nous trouverons un meilleur moyen dans quelques instants.

Créons le jeu.

On commence à parler d'un jeu qui n'existe pas.

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

Le test échoue.

Nous devons créer la classe et la fonction.

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

Nous mettons en œuvre les mots tentés.

Et la solution la plus simple.

Hardcoding comme toujours.

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);    
  }
}

Remarquer

  • Nous stockons les tentatives localement et ajoutons la tentative et modifions également l'implémentation réelle de wordsAttempted().

Nous pouvons implémenter hasLost() s'il manque 6 tentatives.

Avec la mise en œuvre la plus simple comme d'habitude.

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

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

Remarquer

  • Nous apprenons les règles au fur et à mesure que notre modèle grandit.

Comme toujours. Nous arrêtons de faire semblant et décidons de le faire.

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;
  }
}

Nous avons la plupart des mécaniciens.

Ajoutons un dictionnaire de mots valides et jouons invalide.

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

Le test échoue comme prévu.

Nous le réparons.

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();
  }
}

et le test est fixé, mais...

  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  

Remarquer

  • test13, test14 et test15 fonctionnaient auparavant.
  • Maintenant, ils sont cassés depuis que nous avons ajouté une nouvelle règle métier.
  • Nous devons passer le dictionnaire lors de la création du jeu.
  • Nous corrigeons les trois en ajoutant un tableau avec les mots que nous utiliserons.
  • C'est un bon signe que notre configuration devient complexe pour continuer à créer des scénarios valides.

Maintenant, on joue pour gagner.

Nous ajoutons le test et devons modifier hasWon() en conséquence.

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)); 
}

Remarquer

  • Nous n'utilisons aucun drapeau pour vérifier si quelqu'un a gagné. Nous pouvons le vérifier directement.
  • Peu importe qu'il ait gagné lors d'une précédente tentative.
  • Nous faisons un ajouterParamètre refactoriser avec ce nouvel élément aux définitions de jeu précédentes.

Nous avons ajouté le Le bon mot.

Nous devons affirmer que ce mot est dans le dictionnaire.

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");  
  }

Remarquer

  • Nous devions changer tous les jeux précédents car nous devions passer le jeu gagnant avant le début
  • C'est un bon effet secondaire puisqu'il favorise les objets complets et immuables.

✅  test18CorrectWordNotInDictionary
...

✅  test01ValidWordLettersAreValid

  All tests have passed 17/17  

Que se passe-t-il si nous gagnons dans la dernière tentative ?

Les zombies nous demandent toujours de vérifier les limites (B) où se cachent les insectes.

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;
  }

Nous avons tous les mécaniciens.

Ajoutons les positions des lettres.

Nous pouvons le faire en classe Word.

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

Comme d'habitude, on obtient un fonction indéfinie Erreur:

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

Faisons semblant comme d'habitude.

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

Remarquer

  • Les noms sont toujours très importants.
  • On peut nommer le paramètre un autre mot.
  • Nous préférons le bon mot.
  • Nous sommes conscients que nous aurons bientôt besoin d'un algorithme compliqué et que les rôles doivent être clairs et contextuels.

Allons correspondre

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

Échoue.

Il faut mieux le définir

C'est un assez bon algorithme.

Moche et impératif

Nous le refactoriserons plus tard, c'est certain.

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;
}

Et tous les tests passent.

Remarquer

  • La propriété correspondante n'est pas symétrique

Maintenant, nous avons besoin des dernières étapes.

Correspondance dans des positions incorrectes.

et toujours la solution la plus simple...

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 [];
  }
}

Remarquer

  • En ajoutant ces cas zéro sûrs, nous manquons de nombreux bogues habituels.

Un cas test plus épicé.

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));
});

C'est parti pour la réalisation

 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));
    }
  }
}

C'est ça.

Nous avons implémenté un très petit modèle avec toutes les règles significatives.

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);
});

(Vous trouverez plus d'exemples quotidiens dans le dépôt)

J'étais très content de mon mot de travail.

Puis j'ai lu ses règles complexes

Apprendre de nouvelles règles n'est pas un problème lorsque nous avons TDD.

Couvrons les exemples de l'article

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
});

Volons l'algorithme de l'article.

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;
  }

Nous devons ajouter une autre fonction (qui sera utile pour les couleurs du clavier).

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

Remarquer

  • L'algorithme modifie une copie du mot correct en plaçant '*' lorsque la position correcte correspond
  • Il cache également les lettres visitées en changeant en spécial (un '+' invalide).
DREAD vs ADDED
DREA* vs ADDE+
DRE** vs +DDE+
*RE** vs ++DE+
*R*** vs ++D++

Cette solution est différente et plus complète que la précédente.

Les règles du mot n'ont pas changé.

Selon David Farley, nous devons être des experts en apprentissage.

Et nous apprenons en pratiquant des katas comme celui-ci.

Nous nous retrouvons avec 2 classes compactes où nous avons défini notre business model.

Ce petit modèle a une véritable projection 1:1 dans le MAPPER vers le monde réel.

Il est prêt à évoluer.

Ce jeu est une métaphore du vrai génie logiciel.

J'espère que vous le trouverez intéressant et que vous suivrez le kata avec moi.

Vous pouvez jouer avec le repl.it de travail.

  • Combinez cette solution avec la technologie générée par l'IA
  • Utiliser un vrai dictionnaire
  • Changer la langue et l'alphabet
  • Changer les règles en un mot différent

Également publié ici

CHARGEMENT EN COURS
. . . commentaires & Suite!

Leave a Comment

Your email address will not be published. Required fields are marked *