发布于 2026-01-06 0 阅读
0

如何在 JavaScript 中使用 TDD 创建 Wordle 设置 定义单词 创建单词 少量字母 修改当前实现 检查字母过多 重构(或不重构) 有效字母 更多无效字母 重构 比较单词 更多单词 比较不同情况 英语词典 Wordle 游戏 创建游戏对象 尝试的单词 开始猜测 已输 我们输了 我们按词典玩 努力赢 正确单词 输了、赢了、两者都输了? 字母位置匹配 位置错误 使用真实示例 按照复杂规则玩 结论 试试看! 下一步 DEV 的全球展示挑战赛 由 Mux 呈现:展示你的项目!

如何在 JavaScript 中使用 TDD 创建 Wordle

设置

定义一个词

创造一个词

几封信

改变当前的实现方式

检查的字母过多

重构(或不重构)

有效信函

更多无效

重构

词语比较

更多词语

比较不同案例

英语词典

Wordle游戏

创建游戏对象

尝试输入的词语

开始猜吧。

已经输了

我们输掉了比赛

我们按字典行事

全力以赴,争取胜利

正确单词

输了,赢了,还是两者都赢了?

字母位置

匹配

位置错误

用真实例子进行练习

遵守复杂的规则

结论

试试看!

后续步骤

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

我们一直在练习和学习这套精彩的套路。你可以跟着步骤来!

简而言之:JavaScript 也非常适合 TDD(测试驱动开发)。

在 2022 年 1 月的 Wordle rush 活动中,我写了一篇文章,描述了如何使用 PHP 通过 TDD 创建 Wordle

几个月后,我转录了使用 Codex 人工智能创建的 Wordle的 UI 版本。

我将结合这两个世界,以半人马的身份进行编程。

我还会比较不同语言版本的处理过程和输出结果。

这是JavaScript版本。


设置

和往常一样,我们将专注于游戏业务逻辑,因为我们可以使用自然语言命令构建用户界面。

在本文中,我将使用repl.itJest

JavaScript 有许多单元测试框架。

你可以用任何你喜欢的东西。

让我们开始吧……

定义一个词

遵循与上一篇文章相同的原则,我们将首先定义一个 Wordle 词。

Wordle 中最小的信息量是一个单词。

我们可以说这封信更小,但所有需要的信函协议都已经定义好了(我们可能错了)。

单词不是char(5)

单词不是数组

单词不是字符串

这是一个常见的错误,也是双射的违背

单词字符串承担着不同的职责,尽管它们之间可能存在交集

将(无意的)实现细节与(本质的)行为混为一谈是一种普遍存在的错误。

所以我们需要定义什么是词

在 Wordle 中,一个单词是指一个有效的5 个字母的单词。

让我们从快乐的旅程开始:

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

我们断言,提示输入“有效”字母会返回一个字母数组。

结果如下:

Message: letters from word must be 'valid'
Stack Trace:
ReferenceError: Word is not defined
    at Object.<anonymous> (/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)    
Enter fullscreen mode Exit fullscreen mode

这没问题,因为我们还没有定义什么是“词”。

注意

  • 这是TDD模式。
  • 我们甚至在物体存在之前,就根据它们的行为来给它们命名。
  • 词类尚未定义。
  • 我们的圣言的首要责任就是回复来信。
  • 这不是一个获取器。每个单词都必须回答它的字母。
  • 我们并不关心字母排序。那样做就太过优化,而且是过度设计了。
  • 我们先从一个简单的例子开始。没有重复。
  • 我们目前还没有进行单词验证(单词可能是 XXXXX)。
  • 我们可以先从一个更简单的测试开始,验证单词是否被创建。但这会违反测试结构,因为测试结构始终需要断言。
  • 断言中,期望值应该始终是第一个。

创造一个词

我们需要使用 letters() 函数创建一个 Word 对象。

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

注意

  • 我们(目前)还不需要构造函数。
  • 我们把字母功能硬编码到代码中,因为这是目前为止最简单的解决方案。
  • 假装成功,直到我们真的成功。

我们运行了所有测试(只有 1 项),结果正常。

✅  test01ValidWordLettersAreValid

  All tests have passed 1/1  
Enter fullscreen mode Exit fullscreen mode

几封信

我们再编写一个测试:

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

正如预期的那样,测试失败了……

❌  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  
Enter fullscreen mode Exit fullscreen mode

注意

  • 第一次测试通过
  • 第二个测试预期会抛出异常,但并没有。
  • 我们只需声明将引发一个通用异常即可。
  • 我们只需抛出一个通用错误即可。
  • 创建特殊异常是一种会污染命名空间的糟糕代码习惯。(除非我们捕获到它,但目前并没有这样做)。

改变当前的实现方式

我们需要修改我们的实现方式,使 test02(以及 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'];
  }  
}
Enter fullscreen mode Exit fullscreen mode

测试通过了。


✅  test02FewWordLettersShouldRaiseException

✅  test01ValidWordLettersAreValid

  All tests have passed 2/2  
Enter fullscreen mode Exit fullscreen mode

注意

  • 我们(目前)还没有使用构造函数参数来设置实际的字母。
  • 我们只检查几个字母,不会检查太多,因为我们还没有覆盖测试。
  • TDD要求全面覆盖。在没有测试用例的情况下添加额外的检查属于技术规范违规。

检查的字母过多

让我们检查一下数量是否过多

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

});
Enter fullscreen mode Exit fullscreen mode

我们运营它们:

❌  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  
Enter fullscreen mode Exit fullscreen mode

我们添加了验证:

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'];
  }  
}
Enter fullscreen mode Exit fullscreen mode

所有测试均通过。

All tests have passed 3/3  
Enter fullscreen mode Exit fullscreen mode

重构(或不重构)

现在我们可以进行(可选的)重构,将函数改为断言一个范围而不是两个边界。但
我们决定保持现状,因为它更具声明性。

我们还可以根据Zombie 方法添加零词测试

我们开始做吧。

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

});
Enter fullscreen mode Exit fullscreen mode
✅  test04EmptyLettersShouldRaiseException

✅  test03TooManyWordLettersShouldRaiseException

✅  test02FewWordLettersShouldRaiseException

✅  test01ValidWordLettersAreValid

Enter fullscreen mode Exit fullscreen mode

测试通过并不令人意外,因为我们已经有针对这种情况的测试用例。

由于这项测试没有任何价值,我们应该将其移除。


有效信函

现在我们来检查一下什么是有效字母:

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

});
Enter fullscreen mode Exit fullscreen mode

……由于没有引发断言,因此测试失败。


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

Expected constructor: Error

Received function did not throw
Enter fullscreen mode Exit fullscreen mode

我们需要修正代码……

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

由于我们显然是硬编码,所以所有测试都通过了。

All tests have passed 5/5  
Enter fullscreen mode Exit fullscreen mode

注意

  • 我们已将星号硬编码为唯一无效字符(据我们所知)。
  • 我们可以将检查代码放在之前的验证代码之前之后。——在遇到无效情况(包含无效字符和无效长度)之前,我们不能假定行为符合预期。

更多无效

让我们添加更多无效字母来修正代码。

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');
}
Enter fullscreen mode Exit fullscreen mode

注意

  • 我们还没有编写更通用的函数(目前还没有),因为我们无法同时修正测试和重构(技术上不允许我们这样做)。

重构

所有测试结果均正常。

我们可以重构。

我们替换最后两句话。

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');
    //   
}
Enter fullscreen mode Exit fullscreen mode

注意

  • 只有在不同时修改测试的情况下,我们才能进行重构。
  • 该断言仅检查大写字母。因为我们目前只处理这些示例。
  • 我们尽可能推迟设计决策。
  • 我们定义了一个基于英文字母的正则表达式。我们几乎可以肯定它不会接受西班牙语字母(ñ)、德语字母(ë)等。

作为一项检查点,从现在开始我们只能使用五个字母的单词。

让我们对letters()函数进行断言。

我们把它硬编码进去了。

TDD开辟了许多道路。

我们需要跟踪所有这些账户,直到开设新的账户为止。

词语比较

我们需要比较词语

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);
});
Enter fullscreen mode Exit fullscreen mode

测试失败。

让我们使用我们发送给他们的参数。

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

Enter fullscreen mode Exit fullscreen mode
✅  test08TwoWordsAreTheSame

✅  test07TwoWordsAreNotTheSame

✅  test06PointShouldRaiseException

✅  test05InvalidLettersShouldRaiseException

✅  test04EmptyLettersShouldRaiseException

✅  test03TooManyWordLettersShouldRaiseException

✅  test02FewWordLettersShouldRaiseException

✅  test01ValidWordLettersAreValid

  All tests have passed 8/8  
Enter fullscreen mode Exit fullscreen mode

注意

  • 我们存储字母,这就足以进行对象比较(这可能取决于语言)。
  • letters() 函数仍然是硬编码的。

更多词语

我们添加了一个不同的词来进行字母比较。

请注意,之前 letters() 函数是硬编码的。

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

正如预期的那样,测试失败了。

❌  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)
Enter fullscreen mode Exit fullscreen mode

注意

  • 由于许多 IDE 会根据对象打开比较​​工具,因此检查相等性/不等性而不是使用 assertTrue() 非常重要。

  • 这也是使用集成开发环境(IDE)而不要使用文本编辑器的另一个原因。

让我们修改 letters() 函数,因为我们一直在伪造它。

class Word {
  letters() {
      return this._word.split("");
  }  
}
Enter fullscreen mode Exit fullscreen mode

比较不同案例

我们需要确保比较不区分大小写。

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

测试失败。

我们需要做出决定。

我们决定所有域名都使用小写字母。

即使用户界面使用了大写字母,我们也不会允许使用大写字母。

我们不会进行魔法般的转换。

我们修改了测试,使其能够检测并修正无效的大写字母。

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');   
  }
Enter fullscreen mode Exit fullscreen mode

英语词典

我们的词语与英语Wordle词语之间存在双向对应关系,对吗?

我们来试试一个非英语单词。

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

测试失败。

我们无法识别无效的五字母英文单词。

注意

  • 我们需要做出决定。根据我们的双射关系,存在一个外部词典,其中列出了有效的单词。

  • 我们可以在创建单词时使用字典进行验证。但我们希望字典只存储有效的词云单词,不包含任何字符串。

  • 这是先有鸡还是先有蛋的问题。

  • 我们决定处理字典中的无效词,而不是 Wordle 中的词。

  • 我们取消这项测试。

  • 我们马上会找到更好的办法。


Wordle游戏

让我们一起创建游戏吧。

我们开始讨论一款根本不存在的游戏。

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

测试失败。

我们需要创建类和函数。

创建游戏对象

class Game {
  hasWon() {
      return false;
  }  
}
Enter fullscreen mode Exit fullscreen mode

尝试输入的词语

我们实现了尝试过的词语。

最简单的解决方案。

一如既往,采用硬编码。

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

class Game {
  wordsAttempted() {
    return [];
  }
}
Enter fullscreen mode Exit fullscreen mode

✅  test12EmptyGameWordsAttempted
...
  All tests have passed 12/12  
Enter fullscreen mode Exit fullscreen mode

开始猜吧。

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);    
  }
}
Enter fullscreen mode Exit fullscreen mode

注意

  • 我们将尝试次数存储在本地,并添加尝试次数,同时更改 wordsAttempted() 的实际实现。

已经输了

如果尝试失败 6 次,我们可以实现 hasLost() 函数。

一如既往,采用最简单的实现方式。

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

class Game { 
  hasLost() {
      return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

注意

  • 随着模型的不断发展,我们也在学习其中的规则。

我们输掉了比赛

一如既往,我们不再弄虚作假,而是决定付诸行动。

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

我们按字典行事

我们拥有大部分机械师。

让我们添加一个有效的单词词典,然后玩无效单词游戏。

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

测试如预期失败。

我们把它修好了。

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


Enter fullscreen mode Exit fullscreen mode

测试问题已经解决,但是……

  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  
Enter fullscreen mode Exit fullscreen mode

注意

  • test13、test14 和 test15 之前都能正常运行。
  • 现在,由于我们添加了一条新的业务规则,它们都失效了。
  • 创建游戏时需要传递字典。
  • 我们通过添加一个包含我们将要使用的单词的数组来解决这三个问题。
  • 我们的设置变得越来越复杂,从而不断创造出有效的场景,这是一个好兆头。

全力以赴,争取胜利

现在,我们全力以赴,争取胜利。

我们添加了测试,需要相应地修改 hasWon() 函数。

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

Enter fullscreen mode Exit fullscreen mode

注意

  • 我们不使用任何标记来判断谁赢了。我们可以直接检查。
  • 我们不在乎它之前是否赢过。
  • 我们将这个新元素添加到之前的游戏定义中,并对其进行addParameter重构。

正确单词

我们添加了正确的单词

我们需要确认这个词是否在字典里。

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");  
  }
Enter fullscreen mode Exit fullscreen mode

注意

  • 由于我们需要在比赛开始前通过上一局比赛,所以我们需要修改之前所有的比赛。
  • 这是一个不错的副作用,因为它有利于创建完整且不可变的对象。

✅  test18CorrectWordNotInDictionary
...

✅  test01ValidWordLettersAreValid

  All tests have passed 17/17  

Enter fullscreen mode Exit fullscreen mode

输了,赢了,还是两者都赢了?

如果我们在最后一次尝试中获胜会发生什么?

僵尸总是提醒我们检查(B)边界,那里有虫子藏身。

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;
  }
Enter fullscreen mode Exit fullscreen mode

我们拥有所有必要的机械设备。

字母位置

让我们把字母的位置加上去。

我们可以在 Word 课上完成。

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

和往常一样,我们遇到了未定义函数错误:

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

Enter fullscreen mode Exit fullscreen mode

咱们照常装装样子吧。

class Word {
  matchesPositionWith(correctWord) {
    return [];    
  }
}
Enter fullscreen mode Exit fullscreen mode

注意

  • 名字总是非常重要的。
  • 我们可以将参数命名为 anotherWord
  • 我们更喜欢correctWord
  • 我们意识到我们很快就需要一个复杂的算法,因此角色应该清晰明确且与上下文相关。

匹配

我们来匹配一下

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

失败。

我们需要更好地定义它。

这是一个足够好的算法。

丑陋而又势在必行

我们之后肯定会进行重构。

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;
}
Enter fullscreen mode Exit fullscreen mode

所有测试均通过。

注意

  • 匹配属性不对称

位置错误

现在我们需要完成最后几步。

匹配位置错误。

而最简单的解决方案总是……

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 [];
  }
}
Enter fullscreen mode Exit fullscreen mode

注意

  • 添加这些安全、零风险案例后,我们会忽略许多常见的错误。

一个更具挑战性的测试案例。

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));
});
Enter fullscreen mode Exit fullscreen mode

让我们开始实施吧。

 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));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

就是这样。

我们已经实现了一个包含所有有意义规则的小型模型。

All tests have passed 21/21  
Enter fullscreen mode Exit fullscreen mode

用真实例子进行练习

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);
});
Enter fullscreen mode Exit fullscreen mode

2022年9月11日

2022年9月12日

(您可以在代码库中找到更多日常示例)

遵守复杂的规则

我对我的词云作品非常满意。

然后我阅读了有关其复杂规则的文章。

当我们采用测试驱动开发(TDD)时,学习新规则就不是问题

让我们来看看文章中的例子。

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
});
Enter fullscreen mode Exit fullscreen mode

让我们从文章中窃取算法。

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;
  }
Enter fullscreen mode Exit fullscreen mode

我们需要添加另一个功能(对键盘颜色很有用)。

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

lettersAtWrongtPosition(correctWord) {
    return this.matchesIncorrectPositionWith(correctWord).map(position => this.letters()[position -1]);
}
Enter fullscreen mode Exit fullscreen mode

注意

  • 该算法通过在正确位置匹配时放置“*”来更改正确单词的副本。
  • 它还会将访问过的字母替换为特殊的(无效的)“+”号,从而隐藏这些字母。
DREAD vs ADDED
DREA* vs ADDE+
DRE** vs +DDE+
*RE** vs ++DE+
*R*** vs ++D++
Enter fullscreen mode Exit fullscreen mode

结论

这个方案与之前的方案不同,也更加完善

词云规则没有改变。

大卫·法利认为,我们需要成为学习专家。

我们通过练习像这样的套路来学习。

最后我们整理出了 2 个精简的课程,并在其中定义了我们的商业模式。

这个小模型在MAPPER中与现实世界之间存在真正的 1:1 双射。

它已做好进化的准备。

这款游戏是对真实软件工程的一种隐喻。

希望你觉得有趣,并和我一起练习这套套路。

试试看!

你可以尝试使用可运行的repl.it。

后续步骤

  • 将此解决方案与人工智能生成的

  • 使用真正的词典

  • 更改语言和字母表。

  • 将规则更改为不同的词云

文章来源:https://dev.to/mcsee/how-to-create-a-wordle-with-tdd-in-javascript-10ec