初探 ES6:构建和解决数织

GitHub 可以看到两份源代码,其中一份是 ES5 版本,另一份是 ES6 版本。虽然目前火狐 45 已经涵盖了用到的 ES6 语法了,但为了照顾大多数浏览器,本文包括 GitHub Pages 里的 demo 运行的都是 ES5 版本的。

比较两个版本,一个显著的差别是 ES5 版的代码都写在一个匿名函数里,而 ES6 版没有。有人问这样难道不会污染全局空间吗?答案是不会,因为 ES6 引入了 模块 的概念。作为一个模块,它并不是通过 <script> 标签来引入,而是在另一份文件中通过 import 语句来导入所需要的方法和函数。这点是不是和 Python 很像?

‘use strict’;

历史上有过『少写了一个 var 而弄崩了一个网站』的例子。开发者用 Node.js 做服务端,在某个函数中不用 var 声明变量就使用,于是当多个用户共同访问网站时,该变量被绑定在了全局空间上,导致用户数据相互干扰。那么如何防范这一点呢?用 JSLint 等工具审查代码当然也不错,但我将要介绍 严格模式

严格模式并不是 ES6 的新内容,它在 ES5 时代就被引入了。开启严格模式的方法为:在代码顶部或者函数内顶部放置 'use strict'; 。严格模式有着更少的容错率,即非严格模式下能正常执行的语句在严格模式下可能会抛出错误。看起来像自讨苦吃?那我来说说它的好处。

JavaScript 是一种对开发者相当友好的弱类型语言,有很多内置方法即使出现了内部错误,也不会报错,取而代之的是 静默失败 。 多数情况下,它会让你免于处理各式各样的错误;然而事物都是有两面性的,当你的项目出现 bug 却迟迟找不到原因时,它有可能就是罪魁祸首。而严格模式让所有静默失败的操作都抛出错误。像我在上一篇文章里提到的,越能坚持严格的代码风格,就越能回避语法陷阱。关于严格模式的更多介绍,参见 MDN

严于律己。

——中国先贤

抽象

有了之前的铺垫,就可以专注于实现了。想想我要做什么?一种自动解答的数织(NonogramSolve),一种用来玩的数织(NonogramPlay),还有一种能直接编辑网格的数织(NonogramEdit),用来创建各种图案,让人或者机器来解。当然,它们都叫数织,就有一些公共的属性。

我用:

  • grid 来表示数织的网格,是一个二维数组;对于每一个元素,用不同的常量标识它的状态;
  • rowHints 来表示所有行的提示数字;
  • colHints 来表示所有列的提示数字。

为了后面使用方便,再用:

  • m 来表示网格的高度,即行数 rowHints.length
  • n 来表示网格的宽度,即列数 colHints.length

它们也有一些公共的方法,比如说展示部分的。这部分将放到它们的基类中。我创建了一个叫做 Nonogram 的基类,用来存放展示方法。抛开展示部分不谈,NonogramPlayNonogramEdit 两个类几乎不剩下什么了。

我们先来看 NonogramSolve。细致地梳理一遍解答步骤:

  1. 将所有步骤封装到 solve 方法里。
  2. 从第一行开始解起,解完之后顺延到下一行。如果这行已被完全解完,即不存在未确定的方格,那么作个标记,下次来到这里时不必花时间再解一遍。
  3. 所有行解完之后,解第一列;所有列解完之后,解第一行。用函数 updateScanner 控制行列的流程。如果连续地解完了所有的行列,没有一个网格发生变化,那么可以让过程结束,因为即使继续解也不会产生变化。
  4. 用方法 solveSingleLine 封装对每一行或者列的解答过程。传入两个参数,方向和行列数。将这两个参数传给工具方法 getSingleLine,得到该行列的网格状态和提示数字。
  5. 如果网格中不存在未确定的方格,那么用工具方法 checkCorrectness 判断网格和提示数字是否一致。如果是,标记为已解决,否则报错。
  6. 按照提示数字,用 getAllSituations 方法取得网格所有可能的分布情况。对于每一种情况,用 mergeSituation 试着将它整合到先前的结果中。当所有情况都处理完之后,用 setBackToGrid 将解答结果放回网格中去。

最后一步还没有详细说明。我们得先看看单个网格在解答过程中的所有状态。当刚刚获取该行列的网格时,网格中可能存在三种状态:

  • FILLED:已填充;
  • EMPTY:已标空;
  • UNSET:未确定。

其中前两种状态都是某种最终状态,不论如何都不会变化。那么在通过 getAllSituations 方法取得若干种情况时,每种情况又包含了已填充和已标空的各种分布,而这种已填充和已标空必须能被后面的情况所覆盖。所以还要新增另外三种状态:

  • TEMPORARILY_FILLED:暂时填充;
  • TEMPORARILY_EMPTY:暂时标空;
  • INCONSTANT:已出现矛盾。

现在可以说明 mergeSituation 的作用了:每当传入一种情况,与现有的结果进行整合。规则为:

  • 如果出现 TEMPORARILY_FILLED + EMPTYTEMPORARILY_EMPTY + FILLED ,则跳出;
  • UNSET + TEMPORARILY_* = TEMPORARILY_*
  • TEMPORARILY_FILLED + TEMPORARILY_FILLED = TEMPORARILY_FILLED
  • TEMPORARILY_EMPTY + TEMPORARILY_EMPTY = TEMPORARILY_EMPTY
  • TEMPORARILY_FILLED + TEMPORARILY_EMPTY = INCONSTANT
  • 网格中的 FILLEDEMPTYINCONSTANT 都是最终情况,不再变化。

当所有中可能情况都被处理完,执行 setBackToGrid 方法,三种临时状态要被转换为一般状态。TEMPORARILY_FILLED 对应 FILLEDTEMPORARILY_EMPTY 对应 EMPTYINCONSTANT 对应 UNSET

所有流程就是这样。另外,有句话叫做『永远不要信任用户的输入』,假如用户给的提示数字根本就没有解,那应该怎么办?当然有没有解不是一开始就能看出来的,不过可以在方法中多加一点判断,来处理这种特殊情况。

宽以待人。

——中国先贤

在执行 getAllSituations 之前,设置某个表示错误的变量为真。执行 mergeSituation 时,如果未出现 TEMPORARILY_FILLED + EMPTYTEMPORARILY_EMPTY + FILLED 的冲突,就把那个变量设为假。这样就能完美地处理所有合理或不合理的输入了。

我们再来看看 NonogramEdit。这个类的功能相当简单,事实上它的代码也是最短的。它可以按照给定的宽、高、阈值随机生成一个数织;按照网格计算出提示数字;以及响应点击事件,切换被点击方格的状态。按下不表。

最后来看看 NonogramPlay。它的功能只有一个,即响应玩家的点击、拖动事件,然而操作方法却很讲究。我参考了许多数织游戏,选择了一种最自然合理的,然而实现起来略显复杂的操作方式:给玩家两种笔刷,『填充』和『标空』;假如玩家选择了『填充』笔刷,第一笔画在的格子状态为:

  • 已标空,那么程序不会做出任何响应;
  • 未确定,那么笔刷模式为『画笔』,即把接下来碰到的未确定的方格都变为已填充,已标空的和已填充的保持不变;
  • 已填充,那么笔刷模式为『橡皮』,即把接下来碰到的已填充的方格都变为未确定,已标空的和未确定的保持不变。

另外,笔刷能影响的范围也有所限制,即它只能影响接触的第一格和第二格所连成的线上。反过来,假如玩家选择了『标空』笔刷,那么将刚才规则的『已标空』和『已填充』字样对换,就是『标空』笔刷的规则。换句话说,这两种笔刷的地位是对等的。

三种类的功能都分析完了,下面可以着手构建基类 Nonogram 了。基类用来存放工具方法、展示相关方法 。

(下一页:实现)

3 条回复

  1. 博主你来打我呀说道:

    啊好长好长好长

  2. Art9说道:

    二维码的演示效果超级炫酷!对,我就是那种看不懂代码只能看看效果的 (*@ο@*)

发表评论

电子邮件地址不会被公开。 必填项已用*标注