Numble 游戏规则
这个游戏非常简单:猜一个三位数,并且三个数字之和是三的倍数。
例如: 123 是 3 的倍数,因为 1 + 2 + 3 = 6
你有四次机会,没猜一次之后,每个数字的背景颜色都会改变,背景颜色依据数字的位置和是否真的出现在这个数字中来决定。
- 绿色:数字在正确的位置。
- 黄色:数字在结果中但是位置不对。
- 灰色:数字不存在结果中。
这个游戏非常简单:猜一个三位数,并且三个数字之和是三的倍数。
例如: 123 是 3 的倍数,因为 1 + 2 + 3 = 6
你有四次机会,没猜一次之后,每个数字的背景颜色都会改变,背景颜色依据数字的位置和是否真的出现在这个数字中来决定。
为了让构建Numble更容易,我们使用一个小型库Nanny State。这个库是Darren Jones写的,可以看这篇文章了解这个库。Nanny State讲应用所有的数据存储在一个叫State的对象中,所有对State的修改,都会重新渲染在HTML中。由于它的速度和效率以及没有新的语法,它非常简单易学。 首先,我们需要导入 Nanny State 并设置 State, View 和 Update. 要导入它,您只需将这行代码复制到程序的第一行:
import { Nanny,html } from 'https://cdn.skypack.dev/nanny-state'
如果您想了解更多关于您正在导入的内容以及所有内容的结构如何工作的信息,请务必查看上面的链接。接下来,我们需要创建视图:
const View = state => html`
<h1>Numble</h1>`
该 View 函数返回一个字符串,该字符串基本上是我们页面上将显示的 HTML。这是一个让我们开始的基本布局,一旦一切就绪,应该会出现一个“Numble”标题。State 作为一个参数,让我们可以访问存储在单个对象中的数据。
现在我们需要创建 State 对象,这是存储任何数据的地方,但现在它唯一需要的属性是 View将它们链接在一起。
const State = {
View
}
最后,我们需要调用该 Nanny 函数。这将呈现初始视图。我们还将变量分配给 Update 返回值。这将允许我们稍后更新状态。
const Update = Nanny(State)
您的页面应如下所示:

现在我们已经将Nanny State设置好了,下面可以开始制作游戏了。
在我们开始之前有几件事,该函数的好处之一 View 是它使用模板文字,这意味着我们可以在 HTML 本身中添加逻辑。当您想要包含基于状态的不同视图时,这非常方便。不幸的是,标准 if 语句不起作用,我们需要使用三元运算符。
三元运算符的工作方式与 if else 语句相同:条件?如果为真怎么办:如果为假怎么办。简而言之,交换 if a ? 和 else a :。
例如:
const toDo = amIHungry ? "🍰" : "😴"
这相当于;
if (amIHungry){
const toDo = "🍰"
}
else{
const toDo = "😴"
}
三元运算符即将成为您最好的朋友,因此了解它们的工作原理非常重要。
我们先在游戏中添加一个开始按钮。首先在State中添加started。started的初始值为false。现在我们的游戏包含了开始按钮和标题。
State should now look like this: State现在看起来像这样:
const State = {
started: false,
start, finish,
View
}
这里包含了两个变量:start和finish。它们是事件句柄,我们一会就会实现这两个事件。
我们基本上想要两种不同的视图:一种是用户开始时的视图(换句话说,游戏本身),另一种是用户尚未开始时的视图(可能是菜单页面)。所以我们可以使用我们的布尔属性 started 来做到这一点。
const View = state => html`
<h1>Numble</h1>
${state.started ?
html`<button onclick=${state.finish}>END</button>`
:
html`<button onclick=${state.start}>START</button>`
}`
如您所见,标题在三元运算符上方——这意味着它将在程序的两个页面中保持可见。因此,对于三元运算符,您可能已经能够准确地看到它在做什么,但如果没有,请不要担心,它非常简单
它检查 state.started 是否为 true。如果是,则会显示“结束”的按钮。如果不是,则会显示“开始”的按钮。


按钮本身具有内联事件侦听器:“开始”按钮有一个调用函数 start ,“结束”按钮有一个调用函数 finish。显然,这些函数还不存在,这导致我们进入下一个工作:创建事件处理程序。
我们将 start 首先执行该功能。这很简单,因为我们需要做的就是将 started 属性更新为 true. 代码应如下所示:
const start = event => Update({started: true})
这使用了 Update 我们之前编写的函数,并将 State 的值 更改started 为 now true。发生这种情况时,视图将被重新渲染,显示“结束”按钮作为我们的三元运算符的结果。
您可能想 finish 自己编写事件处理程序,因为它的工作原理几乎与 start 函数相同,唯一的区别是 Update 函数正在更改什么。
finish 函数应该如下所示:
const finish = event => Update({started: false})
你现在拥有了世界上最无聊的游戏!
说真的,您现在可以尝试按下开始和结束按钮并与您的游戏互动,即使这不是最引人入胜的体验。
现在,我们可以通过 Numble 最重要的方面之一:数字本身,让我们的游戏更加精彩。
这一步涉及到一些数学和逻辑,但是一旦你了解它,它真的不会太难。函数本身应该是这样的(我将在下面解释发生了什么):
const generateNumber = () => (3*Math.ceil(Math.random()*299+34)).toString()
这是一个箭头函数,它返回一个三位数的三的倍数作为字符串。
具体 来看Math.ceil(Math.random()*299+34),这会生成一个介于 1 和 299 之间的随机数,并使用 将 Math.random() 其四舍五入 Math.ceil。加上 34,然后将其乘以 3,确保该数字是 102 到 999 之间的 3 的倍数,即 3 的 3 位倍数,或“数字”。
最后,整个东西被包裹在一个 .toString() 函数中,把它变成一个字符串。将数字存储为字符串可能看起来很奇怪,但这将更容易在游戏后期为每个单独的数字着色。
我们的下一个挑战是每次用户按下“开始”时显示一个数字。
最好的方法是向状态对象添加一个名为 number. 但是,我们不需要在原始 中执行此操作 State,我们只需要在按下“开始”按钮时执行此操作,因此在 start 事件处理程序中。
这会将我们的 start 函数更改为如下所示:
const start = event => Update({
started: true,
number: generateNumber()
})
新属性 number的值是我们刚刚创建的函数的返回值 generateNumber():随机三位,三的倍数。
为了显示这个,我们需要在 中添加一行 View,特别是在 HTML 部分 when state.started is true, View 现在看起来像这样:
const View = state => html`
<h1>Numble</h1>
${state.started ?
html`<div id="number">${state.number}</div>
<button onclick=${state.finish}>END</button>`
:
html`<button onclick=${state.start}>START</button>`
}`
我们在这里所做的只是添加 a
如果您现在测试代码,每次单击“开始”按钮时都会看到不同的数字,如果将数字相加,您会发现数字是三的倍数!
我们的第一项工作是允许用户进行 4 次猜测。在 Wordle 中,一个 5 个字母的单词允许 6 次猜测,因此对于 Numble,我们将允许 4 次猜测一个三位数字。
为此,我们必须删除该 guess 属性并向 State 对象添加另外两个属性:
const State = {
started: false,
digits: Array(10).fill("grey"),
guesses: Array(4).fill(Array(3).fill(null)),
guessCount: 0,
count: 0,
start, finish, check, appear, remove,
View
}
正如你所看到的,我们现在有一个 guesses 属性来代替 guess 我们之前的属性。的值 guesses 是一个 2D 数组,由 4 个数组组成,每个数组的长度为 3,并用 null. 如果您不熟悉该 Array.fill() 函数,它是创建数组的快捷方式,这意味着我们不必完整地写出数组。
4 个嵌套数组中的每一个都代表用户将做出的 4 个猜测之一。例如,如果第一个猜测是 123,则 guesses 数组将如下所示:
[[1,2,3], [null, null, null], [null, null, null], [null, null, null]]
每次用户进行猜测时,该数组都会更新以匹配他们的猜测,有效地记录他们在游戏中所做的所有猜测。
此外,我们还有一个 guessCount 属性,其值设置为 0。虽然与 count 属性类似,但它可以让我们跟踪用户的猜测次数。
该图应该可以帮助您可视化并完全理解对 count 和 guessCount 属性的需求:

如您所见, guessCount 是猜测存储在哪个嵌套数组的 count 索引,并且是每个猜测的每个单独数字的索引。
现在我们需要对 View 函数进行一些更改:
const View = state => html`
<h1>Numble</h1>
${state.started ?
html`<div id="guesses">
${state.guesses.map((guess, i) => html`<div class="row">${guess.map((number,j)=> html`<div class="grey">${number}</div>`)}</div>`)}
</div>
<p id="feedback">${state.feedback}</p>
<div id="keyboard">
${state.digits.map((digit,index) => html`<button onclick=${state.appear(index)}>${index}</button>`)}
<button onclick=${state.remove}>DELETE</button>
<button onclick=${state.check}>ENTER</button>
</div>
<button onclick=${state.finish}>END</button>`
:
html`<button onclick=${state.start}>START</button>`
}`
这与我们之前创建的几乎相同 View ,但是 id 为“guesses”的 div 已更改。事实上,我们现在使用 2D 数组来像网格一样显示 4 个猜测,我们将需要一个嵌套地图。
编码提示:当使用嵌套地图时,对于每个地图的索引,我们将使用 i 第一个地图和 j 第二个地图。你可以使用任何你认为对你来说最容易的东西,只要它们不一样!
第一个地图循环遍历每个猜测作为网格的一行。然后,第二张地图遍历该猜测的每个单独的数字,并显示相关的 HTML 以显示已被猜测的数字或一个空圆圈。有了这个,你的屏幕应该是这样的:

这种新布局意味着我们还必须更改 appear 和 remove 功能。它相对简单,但同样需要双映射。
const appear = guess => event => {
Update(state => ({
guesses: state.guesses.map((array,i) => i === state.guessCount ? array.map((digit,j) => j === state.count ? guess : digit) : array) ,
count: state.count + 1
}))
}
我们正在 guesses 这里更新属性,这就是拥有两个不同 count 属性将变得非常有用的地方。
第一个映射检查要更改的行:如果数组的索引与用户的猜测匹配,则可以发生第二个映射,否则保持值相同。
第二张地图执行与我们在第二条中创建的完全相同的逻辑 appear 。
就像以前一样,该 remove 功能的工作原理几乎相同。
const remove = event => {
Update(state => ({
guesses: state.guesses.map((array,i) => i === state.guessCount ? array.map((digit,j)=> j === state.count - 1 ? null : digit) : array),
count: state.count - 1
}))
}
remove 这里的第一张地图只是识别用户正在做出的猜测,第二张地图遵循与我们原始函数相同的逻辑 。
然而 count ,属性递减以确保用户可以重新进行猜测。
最后,我们需要对 check 函数进行一些更改。这是每次用户提交猜测时运行的函数。
const check = event => {
Update(state => {
const numble = state.guesses[state.guessCount].join("") === state.number
return {
feedback: numble ? "NUMBLE!" : state.guessCount < 3 ? "Keep going..." : `Nope! It was ${state.number}`,
guessCount: state.guessCount + 1,
count: 0
}
})
}
这里只有两件事发生了变化,并且都在返回的对象中。该 feedback 属性添加了一些逻辑来使应用程序更具动态性。反馈现在将显示一条消息,让用户知道他们的进展情况。
在这种情况下,我们有:如果 numble 是 true ,换句话说,如果用户的猜测是正确的,则反馈变为“NUMBLE”;如果 numble 是 false,检查猜测是否小于三(这实质上是检查用户是否做出了最终猜测)。如果是,则反馈为“继续……”,否则为“不!是(答案)”。
现在开始变得有点棘手,在我们开始之前,检查您是否熟悉该 Array.map() 函数可能是个好主意。与三元运算符一样,它们将成为您在这一步和下一篇文章中最好的朋友,因为 Numble 需要大量的数组映射来处理状态中的数据。
如果您的 Array.map() 技能有点生疏,或者您甚至没有听说过,请不要担心,它们很容易理解,您可以在此处阅读更多关于它们的信息。
此步骤包含三个主要部分:
创建虚拟键盘 显示用户的猜测 检查用户的猜测是否正确 尽管它们都相互依赖,但如果将所有内容分解成更小的块,则更容易理解。
首先,我们需要在 中添加我们将要使用的函数和另外三个属性 State:
const State = {
started: false,
digits: Array(10).fill("grey"),
guess: Array(3).fill(null),
count: 0,
start, finish, remove, check, appear,
View
}
按照这个顺序处理它们, digits 现在的值是一个长度为 10 的数组,每个空格都用字符串“grey”填充。这是因为我们将使用它来跟踪游戏中每个数字应该是什么颜色,并且数组的索引将代表从 0 到 9 的每个可能的数字。
的初始值 guess 也是一个长度为 3 的数组,每个空格填充 null.
最后, count 设置为 0,这将用于记录玩家猜到了多少位数。
我们将映射 digits 数组以创建我们的屏幕键盘,所以我们需要添加一些东西到 View. 虽然在我们这样做之前,我们需要摆脱
const View = state => html`
<h1>Numble</h1>
${state.started ?
html`<div id="guesses">
${state.guess.map(number => html`<div>${number}</div>`)}
</div>
<div id="keyboard">
${state.digits.map((digit,index) => html`<button onclick=${appear(index)}>${index}</button>`)}
<button onclick=${remove}>DELETE</button>
<button onclick=${check}>ENTER</button>
</div>
<button onclick=${finish}>END</button>`
:
html`<button onclick=${start}>START</button>`
}`
代替
在“猜测”
这是一个应该是什么样子的示例(使用我的 CSS):

最终,当数组中每一项的值发生变化时,显示的内容也会随之变化以匹配。
在“键盘”中,
${state.digits.map((digit,index) => html`<button onclick=${state.appear(index)}>${index}</button>`)}
这映射到长度为 10 的数组,为每个项目创建一个按钮并显示每个项目的按钮 index 。换句话说,数字 0 到 9。每个按钮还有一个内联事件侦听器,它调用事件处理程序 appear 并 index 作为参数提供。但是,我们稍后将对此进行全面探讨。
然后,我们有两个按钮,一个称为“删除”,另一个称为“输入”。它们都有内联事件侦听器,它们调用各自的事件处理程序 remove 和 check. 再一次,我们将在稍后充分探索这些。
首先,这是您的键盘外观的示例:

查看 appear 事件处理程序,我们希望此函数显示玩家在 guess.
const appear = guess => event => {
Update(state => ({
guess: state.guess.map((digit,index) => index === state.count ? guess : digit),
count: state.count + 1
}))
}
首先,这个事件处理程序和我们之前做的唯一区别是这个函数有一个额外的参数 guess。这是 作为参数提供index 的 数组的 。digits换句话说,它是玩家点击的数字。
该 Update 功能看起来有点不同。这是因为它需要访问状态,所以它提供了一个箭头函数,将旧状态映射到新状态(Nanny State 将这些称为“转换函数”)
就它实际更新的内容而言,该 属性映射在三个s guess 的原始数组上 ,如果 项目的 等于 (猜测的位置),则将值 替换 为用户单击的按钮)。如果 不等于 ,则项目的值保持原样: 。nullindexcountnullguessindexcountnull
然后它增加 count 1,允许用户将他们的第二个猜测输入到第二个空格中。
这是用户单击几个数字后该行的外观:

事件 remove 处理程序(具有讽刺意味)几乎相同:
const remove = event => {
Update(state => ({
guess: state.guess.map((digit,index) => index === state.count - 1 ? null : digit),
count: state.count - 1
}))
}
按照 appear 函数的逻辑,您应该能够弄清楚这里发生了什么,但如果没有,请不要担心。它 guess 通过映射原始数组进行更新,如果 index 等于之前的猜测次数(即计数 - 1),它会将项目的值替换为 null,从而有效地删除猜测。
而这一次,它减 count 一,让用户继续猜测。
只是 check 要执行的功能。
‘Enter’ 按钮的事件处理程序被调用 check,我们希望它(惊喜)检查用户的猜测是否正确,但我们也希望它重置猜测,以便用户可以重试。
该函数如下所示:
const check = event => {
Update(state => {
const numble = state.guess.join("") === state.number
return {
feedback: numble ? "NUMBLE!" : "Wrong!",
guess: Array(3).fill(null),
count: 0
}
})
}
和以前一样,它 Update 使用了一个转换器函数并 state 作为一个参数,让我们可以直接访问状态中保存的所有应用程序数据。然后它会创建一个名为 的布尔常量 numble。它可能看起来不像,但 state.guess.join("") === state.number 实际上是一个条件(检查用户的猜测是否等于我们生成的数字),如果满足此条件,则值为 , numble 否则 true 为 false。
然后它返回状态的三个更新属性:
我们的最后一步是放入一些 HTML View 以便显示反馈。
放置它的好地方是猜测下方和键盘上方。所以,你的决赛 View 应该是这样的:
const View = state => html`
<h1>Numble</h1>
${state.started ?
html`<div id="guesses">
${state.guess.map(number => html`<div>${number}</div>`)}
</div>
<p id="feedback">${state.feedback}</p>
<div id="keyboard">
${state.digits.map((digit,index) => html`<button onclick=${state.appear(index)}>${index}</button>`)}
<button onclick=${state.remove}>DELETE</button>
<button onclick=${state.check}>ENTER</button>
</div>
<button onclick=${state.finish}>END</button>`
:
html`<button onclick=${state.start}>START</button>`
}`
如果您愿意,您可以 feedback 在游戏开始时设置消息,例如在 start 事件处理程序中,您可以添加 feedback 带有字符串值的属性(“猜 3 位数字”):
const start = event => {
Update({
started: true,
number: generateNumber(),
feedback: "Guess 3 digits"
})
}
就是这样!你现在有一个功能齐全的猜数字游戏!
在继续阅读第二篇文章之前,这里只有一些关于 CSS 和 bug 的注释。
如果您是一名优秀的程序员,您可能会发现这里的一些错误,例如,如果用户在猜到三位数字之前点击“Enter”怎么办?如果你开始玩它,你肯定会注意到更多。
它们根本不难修复,您只需要在适当的情况下满足几个条件。例如,要解决在三位数之前检查的问题,可以在 check 函数中编写:
const check = event => {
Update(state => {
const numble = state.guess.join("") === state.number
return state.count < 3 ? {
feedback: "too short"
}
:
{
feedback: numble ? "NUMBLE!" : "Wrong!",
guess: Array(3).fill(null),
count: 0
}
})
}
这只是检查猜测的数量是否小于 3,并相应地返回具有不同值的不同属性。
我们现在有一个功能齐全的“猜数字”游戏,接下来我们将使它更像完整的 Numble。
正如文章开头所述,颜色是 Wordle 的主要焦点,因此也是 Numble。如果您还没有玩过 Numble 或 Wordle ,强烈建议您这样做,以便正确理解颜色的工作方式。
这是 Numble 使用的着色系统示例:

用户猜测后,颜色会在两个位置更新:实际猜测和键盘上。两者的逻辑完全相同,因此我们可以创建一个名为的箭头函数 getColors ,将猜测和实际数字作为参数。
const getColors = (guess,number) => guess.map((digit,index) => number.includes(digit) ? digit.toString() === number[index] ? "green" : "yellow": "black")
我们映射 ‘guess’ 数组并使用 ‘String.includes(item)’ 方法,我们首先检查答案是否包含猜测的数字。如果是,那么我们检查数字是否在正确的位置。如果是,则颜色被指定为“绿色”。如果不是,则颜色为“黄色”。否则,该数字根本不在答案中,因此颜色为“黑色”。
然后,此箭头函数应返回一个数组,其中包含“绿色”、“黄色”或“黑色”三个项目,对应于“猜测”中的每个数字。
例如,如果我们使用函数调用函数, getColors([1,2,3], “327”) 那么我们应该返回的数组是 [“black”, “green”, “yellow”]
您可能会注意到我们必须将数字更改为字符串。这是因为我们需要将它与存储为字符串的答案进行比较,如果两个元素的类型不同,则无法比较它们。好吧,您可以尝试,但要准备好进入整个 JavaScript 类型强制痛苦的世界。
注意:Wordle 以不同的方式处理重复,所以如果你想让这更难一点,你可以尝试模仿 Wordle 的方法。
对于下一部分,我们不必对 进行任何更改或添加任何内容 State,但 View 确实会变得更复杂一些。正如第一篇文章中简要提到的,我们将使用 CSS 类来更改颜色。
const View = state => html`
<h1>Numble</h1>
${state.started ?
html`<div id="guesses">
${state.guesses.map((guess, i) => html`<div class="row">${guess.map((number,j)=> html`<div class=${state.guessCount > i ? getColors(guess,state.number)[j] : "grey"}">${number}</div>`)}</div>`)}
</div>
<p id="feedback">${state.feedback}</p>
<div id="keyboard">
${state.digits.map((digit,index) => html`<button class=${digit} onclick=${state.appear(index)}>${index}</button>`)}
<button onclick=${state.remove}>DELETE</button>
<button onclick=${state.check}>ENTER</button>
</div>
<button onclick=${state.finish}>END</button>`
:
html`<button onclick=${state.start}>START</button>`
}`
如您所见,唯一改变的两件事是键盘按钮和每行的各个部分的 CSS 类。
从“guesses” div 开始,我们有以下逻辑:
state.guessCount > i ? getColors(guess,state.number)[j] : "grey"
首先检查是否 guessCount 高于索引,这是为了确保每次重新呈现页面时,任何先前的猜测都会重新着色。如果需要颜色,我们 getColors 以用户的猜测和答案作为参数调用该函数,并在每个数字的索引处获取项目, j.
以下是用户猜测后您的屏幕应该是什么样子:

该 getColors 函数的数组是:
["yellow", "black", "black"]
因此,用户现在将知道 3 在数字中但在错误的位置,而 4 和 5 根本不在数字中。
键盘逻辑要简单得多,但它仍然使用 getColor 我们之前编写的相同功能。还记得之前我们是如何 digits 用“灰色”填充数组的吗?这就是我们这样做的原因。
当键盘在屏幕上绘制时,类就是 digits 数组中键索引处的值。稍后我们将介绍如何更改颜色,但是在第一次猜测之后使用上面的示例, digits 数组应该如下所示:
["grey", "grey", "grey", "yellow", "black", "black", "grey", "grey", "grey", "grey"]
我们快到了!我们最后的工作是改变 check 功能。
const check = event => {
Update(state => {
const guess = state.guesses[state.guessCount]
const numble = guess.join`` === state.number
const colors = getColors(guess,state.number)
return {
feedback: numble ? "NUMBLE!" : state.guessCount < 3 ? "Keep going..." : `Nope! It was ${state.number}`,
digits: state.digits.map((colour,digit) => guess.includes(digit) ? colors[guess.indexOf(digit)] : colour),
guessCount: state.guessCount + 1,
count: 0
}
})
}
在 Update 函数中,还有两个常量。这只是使返回对象中的逻辑变得容易。
我们有 guess which 是用户刚刚猜到的三位数数组(因此使用 state.guessCount)。我们 numble 之前也有,但这次使用的是 guess 我们刚刚创建的常量。这只是有助于拥有更清晰的代码并避免重复。最后,我们有 colors 哪个是 getColors 函数运行时返回的数组,用户当前的猜测和答案。
这将更新数字数组并确保键盘上的数字在每次猜测后正确着色。
现在,返回对象与上面的对象相同,但我们也在更新 digits 属性。
state.digits.map((color,digit) => guess.includes(digit) ? colors[guess.indexOf(digit)] : color)
这是我们最终的映射功能!它本质上检查键盘上的数字(即 digit)是否在猜测中。如果是当前颜色,则应替换为 getColors 函数生成的颜色,否则颜色应保持不变。
使用与上面相同的猜测,我们可以看到键盘应该是什么样子:

就是这样!Numble 的全功能版本!
在 Numble 的实际版本中,我添加了许多功能,只是为了让游戏更加动态。如果你想挑战自己并添加一些额外的功能,这里有一些来自我的 Numble 最终版本的建议:
我真的希望你和我一样在制作 Numble 时玩得开心!