65.9K
CodeProject 正在变化。 阅读更多。
Home

在 JavaScript 中建模一个逻辑谜题

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (5投票s)

2017年7月13日

CPOL

24分钟阅读

viewsIcon

19697

如何在 JavaScript 中建模逻辑网格谜题

目录

引言

本文解释了如何在 JavaScript 编程语言中为逻辑网格谜题建模。逻辑网格谜题,也称为逻辑谜题或逻辑问题,可以在 Dell MagazinesPenny Press 的杂志中找到。Mystery Master 网站 http://www.mysterymaster.com 致力于编写可以解决逻辑谜题的软件。可以把它想象成 "天网",但没有消灭人类的意图。本文将重点介绍逻辑谜题 "五座房子"。这个两星谜题的早期版本被称为 "爱因斯坦的谜语" 或 "斑马谜题"。这个版本的谜题出现在思维缜密的 Marilyn vos Savant 的专栏中。在继续阅读之前,请先查看这个逻辑谜题。您应该具备 JavaScript 的基本知识,了解一些新的构造,并理解 JavaScript 中的类如何工作。

JavaScript

JavaScript 是网络的客户端语言。我不认为它是最好的语言,但多年来它已经有所改进。我希望它是强类型的,有更好的作用域规则(public、private 等),但我想我可以使用 TypeScript。它不能做我在 C# 中可以做的一些好事情,比如在实例化对象时初始化它。它没有 空合并运算符 ??。它也非常脆弱——您的 IDE 和/或浏览器可能无法发现您的错误。但抱怨够了……以下是我喜欢的一些东西。

  1. "use strict"; 选项。
  2. 使用 const 关键字定义常量。
  3. 使用 let 关键字避免变量提升。
  4. addEventListener 方法,以便事件调用的方法可以是局部的。
  5. 返回闭包的能力——引用局部变量的函数。有关闭包的更多信息,请参阅 智能链接智能规则 部分。
  6. 在本地存储中存储/检索信息的能力。

什么是逻辑谜题

逻辑谜题是一个神秘的故事。但你不仅仅是它的读者,你还是它最重要的角色——侦探!就像任何值得拥有望远镜的侦探一样,你必须解开谜团。大多数逻辑谜题都有其属性,例如标题、作者和星级。一星表示谜题容易,五星表示谜题非常困难。每个谜题还配有一幅幽默的图片。虽然这些属性很重要,但它们不会帮助你解决逻辑谜题。真正有帮助的是谜题的线索。线索通常以编号列表的形式给出,但有时也可以在介绍中找到。属性、介绍和线索列表是逻辑谜题的文本

注意:如果你想自己解决一个逻辑谜题,你可以使用以下工具:图表网格。这两种形式将在未来的文章中讨论。

要为逻辑谜题建模,必须将逻辑谜题的文本解析为特定类型的数据。解决逻辑谜题所需的一切都必须由这些数据捕获。在继续进行时,有一件重要的事情要记住

逻辑谜题中名词之间的所有关系都必须表示为事实或规则。

请在阅读谜题文本时记住以下问题。

  • 谜题中的对象是什么?这些是名词
  • 名词之间的关系是什么?这些由动词链接表示。
  • 事实是什么?这些将名词、动词和链接组合成静态语句。
  • 规则是什么?这些是条件语句。并非所有谜题都有规则,包括这个谜题。

我们需要数据结构来存储每种类型的数据,当我们在面向对象编程的世界中谈论数据结构时,我们谈论的是。那么,我们应该将存储逻辑谜题所有数据的类命名为什么呢?当然是Puzzle

谜题

Puzzle 类是每个谜题模块的父类。该类执行许多任务。它有方法将数据加载到每种类型对象的数组中。它有一个方法来验证数据。它需要使这些数据可供查看和解决谜题的类使用。

Puzzle 类

/* global Q, NL, NounType, Verb, IsNot, Maybe, Is, Link, 
 * With, Fact, Rule, SmartLink, Mark, Solver */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";
/**
 * @class Puzzle class.
 * @description The Puzzle class is the base class for all puzzle modules.
 * @author Michael Benson <michael.benson@mysterymaster.com>
 * @copyright Mystery Master
 * @version 2017-03-27
 * @returns {Puzzle} Puzzle.
 */
function Puzzle() {
	/** Name of the puzzle module. */
	this.myName = null;
	
	/** Title of the puzzle. */
	this.myTitle = null;
	/** Array of noun types. */
	this.nounTypes = [];
	
	/** Array of verbs. There must always be three verbs. */
	this.verbs = [];
	
	/** Array of links. */
	this.links = [];
	
	/** Array of facts. */
	this.facts = [];
	
	/** Array of rules. */
	this.rules = [];
	/** Maximum number of noun types. */
	this.maxNounTypes = 0;
	
	/** Maximum number of nouns per type. 
        This must be the same for all noun types. */
	this.maxNouns = 0;
	
	/** Maximum number of links. */
	this.maxLinks = 0;
	
	/** Maximum number of facts. */
	this.maxFacts = 0;
	
	/** Maximum number of rules */
	this.maxRules = 0;
	/** Puzzle validation flag. Set to true in the validation method. */
	this.isValid = false;
	
	// ---------------------------------------------------------------------------------
	this.toString = function () {
		return this.myTitle === null ? "Puzzle" : this.myTitle;
	};
	this.asString = function () { return this.toString(); };
	/**
	 * Creates and appends the new noun type to the nounTypes array.
	 * @param {string} name Name of the noun type.
	 * @returns {NounType} Noun Type object.
	 */
	this.addNounType = function (name) {
		let nounType = new NounType(this.nounTypes.length, name);
		this.nounTypes.push(nounType);
		this.maxNounTypes = this.nounTypes.length;
		if (nounType.num === 1) this.maxNouns = nounType.nouns.length;
		return nounType;
	};
	/**
	 * Creates and appends the new link to the links array.
	 * @param {string} name Name of the link.
	 * @param {NounType} nounType Noun Type.
	 * @returns {Link} Link object.
	 */
	this.addLink = function (name, nounType) {
		let link = new Link(this.links.length, name, nounType);
		this.links.push(link);
		this.maxLinks = this.links.length;
		return link;
	};
	/**
	 * Returns the clue number in parenthesis from the clueNum or the name.
	 * @param {string|null} clueNum Clue number.
	 * @param {string} name Name of the fact or rule.
	 * @returns {string} String.
	 */
	function getClueNumMsg(clueNum, name) {
		if (clueNum === null || clueNum.length < 1) return name;
		let i = name.length - 1;
		if (i < 0) return name;
		let eos = name[i];
		let txt = "";
		if (clueNum[0] === 'A') {
			let tmp = clueNum.length > 1 ? " " + clueNum.substring(1) : "";
			txt = "analysis" + tmp;
		}
		else if (clueNum[0] === '0') {
			txt = "intro";
		}
		else {
			let tmp = clueNum.indexOf(",") > -1 ? "s" : "";
			txt = "clue" + tmp + " " + clueNum;
		}
		let msg = name.substring(0, i) + " (" + txt + ")" + eos;
		return msg;
	}
	/**
	 * Creates and appends the new fact to the facts array.
	 * Overloads: nounA and/or nounB can be one noun or a list of nouns.
	 * @param {string} clueNum Clue number.
	 * @param {Noun} nounA Noun A.
	 * @param {Verb} verb Verb.
	 * @param {Link} link Link.
	 * @param {Noun} nounB Noun B.
	 * @param {string|null} name Name of the fact, or null.
	 * @param {boolean} initEnabled True if fact should be initially enabled, 
     *  otherwise false.
	 * @returns {Fact} Fact object.
	 */
	this.addFact = function (clueNum, nounA, verb, link, 
                   nounB = null, name = null, initEnabled = true) {
		if (nounB === null)
			this.addFacts1(clueNum, nounA, verb, link, name, initEnabled);
		else
			this.addFacts2(clueNum, nounA, verb, link, nounB, name, initEnabled);
	};
	/**
	 * Creates and appends the new rule to the rules array.
	 * @param {string} clueNum Clue number.
	 * @param {string} name Name of the rule.
	 * @param {Array|null} nouns Array of nouns referenced in the rule.
	 * @param {boolean} initEnabled True if fact should be initially enabled, 
     * otherwise false.
	 * @returns {Rule} Rule object.
	 */
	this.addRule = function (clueNum, name, nouns = null, initEnabled = true) {
		let msg = getClueNumMsg(clueNum, name);
		let rule = new Rule(this.rules.length, msg, nouns, initEnabled);
		this.rules.push(rule);
		this.maxRules = this.rules.length;
		return rule;
	};
	/**
	 * Returns the noun type given by its one-based number.
	 * @param {number} num One-based number of the noun type.
	 * @returns {NounType} Noun Type object.
	 */
	this.getNounType = function (num) {
		return this.nounTypes[num - 1];
	};
	/**
	 * Returns the noun give by its one-based type and one-based number.
	 * @param {number} typeNum One-based number of the noun's type.
	 * @param {number} num One-based number of the noun.
	 * @returns {Noun} Noun object.
	 */
	this.getNoun = function (typeNum, num) {
		return this.nounTypes[typeNum - 1].nouns[num - 1];
	};
	/**
	 * Returns the verb given by its number, either -1, 0, or 1.
	 * @param {number} num Number of the verb.
	 * @returns {Verb} Verb object.
	 */
	this.getVerb = function (num) {
		return this.verbs[num + 1];
	};
	// --------------------------------------------------------------------------------
	/**
	 * Returns "proper English" for the fact. 
     * The child class should override this method.
	 * @param {Noun} noun1 Noun 1.
	 * @param {Verb} verb Verb.
	 * @param {Link} link Link.
	 * @param {Noun} noun2 Noun 2.
	 * @returns {string} String.
	 */
	this.sayFact = function (noun1, verb, link, noun2) {
		return "PARENT " + noun1.name + " " + verb.name + " " + 
                link.name + " " + noun2.name + ".";
	};
	/**
	 * Returns the nouns in nouns1 that are not in nouns2.
	 * @param {Array} nouns1 Array of nouns.
	 * @param {Array} nouns2 Array of nouns.
	 * @returns {Array} Array of nouns in nouns1 that are not in nouns2.
	 */
	this.getArrayExcept = function (nouns1, nouns2) {
		let nouns = [];
		for (let noun1 of nouns1) {
			let found = false;
			for (let noun2 of nouns2) {
				if (noun1 === noun2) {
					found = true;
					break;
				}
			}
			if (!found) nouns.push(noun1);
		}
		return nouns;
	};
	/**
	 * Creates and appends the new fact to the facts array.
	 * Note: If  "self" was used to call sayFact, then the parent's sayFact is used!
	 * @param {string} clueNum Clue number.
	 * @param {Noun} noun1 Noun 1.
	 * @param {Verb} verb Verb.
	 * @param {Link} link Link.
	 * @param {Noun} noun2 Noun 2.
	 * @param {string|null} name Name of the fact, or null.
	 * @param {boolean} initEnabled True if fact should be initially enabled, 
     * otherwise false.
	 * @returns {Fact} Fact object.
	 */
	this.addOneFact = function (clueNum, noun1, verb, link, 
                      noun2, name = null, initEnabled = true) {
		let txt = name;
		if (name === null || name.length < 1) {
			txt = this.sayFact(noun1, verb, link, noun2);
		}
		// Don't enter duplicate facts.
		let ok = true;
		for (let oldFact of this.facts) {
			if (oldFact.verb !== verb) continue;
			if (oldFact.noun1 === noun1 && oldFact.link === link && 
                                           oldFact.noun2 === noun2)
				ok = false;
			else if (oldFact.noun1 === noun2 && oldFact.link === link && 
                     oldFact.link.num === 0 && oldFact.noun2 === noun1)
				ok = false;
			if (!ok) {
				console.log("Warning! This fact already exists: " + 
                             oldFact.num + " " + oldFact.name);
				return null;
			}
		}
		let msg = getClueNumMsg(clueNum, txt);
		let fact = new Fact(this.facts.length, msg, 
                   noun1, verb, link, noun2, initEnabled);
		this.facts.push(fact);
		this.maxFacts = this.facts.length;
		return fact;
	};
	/**
	 * Creates and appends the new fact to the facts array.
	 * Note: If I define this function as local and use "self" to call addFact, 
     * then the parent's sayFact is used!
	 * @param {string} clueNum Clue number.
	 * @param {Array} nouns Array of nouns.
	 * @param {Verb} verb Verb.
	 * @param {Link} link Link.
	 * @param {string|null} name Name of the fact(s), or null.
	 * @param {boolean} initEnabled True if fact should be initially enabled, 
     * otherwise false.
	 * @returns {Fact} Fact object.
	 */
	this.addFacts1 = function (clueNum, nouns, verb, link, 
                               name = null, initEnabled = true) {
		for (let i = 0; i < nouns.length - 1; i++) {
			let noun1 = nouns[i];
			for (let j = i + 1; j < nouns.length; j++) {
				let noun2 = nouns[j];
				if (noun1 === noun2 || (link === With && 
                    noun1.type === noun2.type)) continue;
				this.addOneFact(clueNum, noun1, verb, 
                                link, noun2, name, initEnabled);
			}
		}
	};
	/**
	 * Creates and appends the new fact to the facts array. 
     * Overload: nounA and nounB can each be one noun or a list of nouns.
	 * Note: If I define this function as local and use "self" to call addFact, 
     * then the parent's sayFact is used!
	 * @param {string} clueNum Clue number.
	 * @param {Noun} nounA Noun A.
	 * @param {Verb} verb Verb.
	 * @param {Link} link Link.
	 * @param {Noun} nounB Noun B.
	 * @param {string|null} name Name of the fact(s), or null.
	 * @param {boolean} initEnabled True if fact should be initially enabled, 
     * otherwise false.
	 * @returns {Fact} Fact object.
	 */
	this.addFacts2 = function (clueNum, nounA, verb, link, nounB, 
                     name = null, initEnabled = true) {
		let nouns1 = Array.isArray(nounA) ? nounA : [nounA];
		let nouns2 = Array.isArray(nounB) ? nounB : [nounB];
		for (let noun1 of nouns1) {
			for (let noun2 of nouns2) {
				if (noun1 === noun2 || (link === With && 
                    noun1.type === noun2.type)) continue;
				this.addOneFact(clueNum, noun1, verb, link, noun2, name, initEnabled);
			}
		}
	};
	/**
	 * Creates and appends new facts to the facts array.
	 * @param {string} clueNum Clue number.
	 * @param {Array} nouns Array of nouns.
	 * @param {Verb} verb Verb.
	 * @param {Link} link Link.
	 * @param {string|null} name Name of the fact(s), or null.
	 * @param {boolean} initEnabled True if fact should be initially enabled, 
     * otherwise false.
	 */
	this.addFactsInSequence = function (clueNum, nouns, verb, 
                              link, name = null, initEnabled = true) {
		for (let i = 0; i < nouns.length - 1; i++) {
			this.addOneFact(clueNum, nouns[i], verb, 
                            link, nouns[i + 1], name, initEnabled);
		}
	};
	/**
	 * Creates and appends new facts to the facts array.
	 * Overload: nounA and nounB can each be a list of nouns or a noun type.
	 * @param {string} clueNum Clue number.
	 * @param {Noun} nounA Noun A.
	 * @param {Verb} verb Verb.
	 * @param {Link} link Link.
	 * @param {Noun} nounB Noun B.
	 * @param {string|null} name Name of the fact(s), or null.
	 * @param {boolean} initEnabled True if fact should be initially enabled, 
     * otherwise false.
	 */
	this.addFactsOneToOne = function (clueNum, nounA, verb, 
                            link, nounB, name = null, initEnabled = true) {
		let nouns1 = Array.isArray(nounA) ? nounA : nounA.nouns;
		let nouns2 = Array.isArray(nounB) ? nounB : nounB.nouns;
		let n = nouns1.length;
		if (n !== nouns2.length) return;
		for (let i = 0; i < n; i++) {
			this.addOneFact(clueNum, nouns1[i], verb, 
                 link, nouns2[i], name, initEnabled);
		}
	};
	/**
	 * Creates and appends new facts to the facts array.
	 * @param {string} clueNum Clue number.
	 * @param {Noun} noun1 Noun 1.
	 * @param {NounType} nounType2 Noun type 2.
	 * @param {boolean} flag Flag.
	 * @param {char} ch Character.
	 * @param {string|null} name Name of the fact(s), or null.
	 * @param {boolean} initEnabled True if fact should be initially enabled, 
     * otherwise false.
	 */
	this.addFactsStartsWith = function 
        (clueNum, noun1, nounType2, flag, ch, name = null, initEnabled = true) {
		for (let noun2 of nounType2.nouns) {
			if ((noun2.name[0] === ch) === flag) {
				this.addOneFact(clueNum, noun1, IsNot, With, noun2, name, initEnabled);
			}
		}
	};
	/**
	 * Creates and appends new facts to the facts array.
	 * @param {string} clueNum Clue number.
	 * @param {NounType} nounType1 Noun type 1.
	 * @param {NounType} nounType2 Noun type 2.
	 * @param {boolean} flag Flag.
	 * @param {string|null} name Name of the fact(s), or null.
	 * @param {boolean} initEnabled True if fact should be initially enabled, 
     * otherwise false.
	 */
	this.addFactsIsNotFirstChar = function (clueNum, nounType1, 
         nounType2, flag, name = null, initEnabled = true) {
		for (let noun1 of nounType1.nouns) {
			for (let noun2 of nounType2.nouns) {
				if ((noun1.name[0] === noun2.name[0]) === flag) {
					this.addOneFact(clueNum, noun1, IsNot, With, 
                                    noun2, name, initEnabled);
				}
			}
		}
	};
	/**
	 * Creates and appends new facts to the facts array.
	 * @param {string} clueNum Clue number.
	 * @param {Array} nouns Array of nouns.
	 * @param {Link} link Link.
	 * @param {string|null} name Name of the fact(s), or null.
	 * @param {boolean} initEnabled True if fact should be initially enabled, 
     * otherwise false.
	 */
	this.addFactsNotConsecutive = function (clueNum, nouns, link, 
                                  name = null, initEnabled = true) {
		let n = nouns.length;
		let type = link.nounType;
		let max = type.nouns.length;
		if (2 * n - 1 === max) {
			for (let noun of nouns) {
				for (let i = 1; i < max; i += 2) {
					let slot = type.nouns[i];
					this.addOneFact
                    (clueNum, noun, IsNot, With, slot, name, initEnabled);
				}
			}
		}
		else {
			for (let i1 = 0; i1 < n - 1; i1++) {
				let noun1 = nouns[i1];
				for (let i2 = i1 + 1; i2 < n; i2++) {
					let noun2 = nouns[i2];
					this.addOneFact(clueNum, noun1, IsNot, 
                                    link, noun2, name, initEnabled);
					this.addOneFact(clueNum, noun2, IsNot, link, 
                                    noun1, name, initEnabled);
				}
			}
		}
	};
	// ----------------------------------------------------------------------------------
	// Validate Puzzle.
	/** Viewer object. Instantiated on the UI thread. */
	let viewer = null;
	
	/** Solver object. Instantiated on the WW thread. */
	let solver = null;
	
	/**
	 * Validates the puzzle. If valid, the isValid flag is set to true, otherwise false.
	 * @param {Viewer} aviewer Viewer object.
	 * @param {Solver} asolver Solver object.
	 */
	this.validate = function (aviewer = null, asolver = null) {
		viewer = aviewer;
		solver = asolver;
		
		//print("Validate the properties.");
		if (this.myName == null || this.myName.length === 0) 
            throw new Error("The name of the puzzle must be given!");
		if (this.myTitle == null || this.myTitle.length === 0) 
            throw new Error("The title of the puzzle must be given!");
		this.maxNounTypes = this.nounTypes.length;
		//print("Validate the nouns. maxNounTypes=" + this.maxNounTypes);
		if (this.maxNounTypes < 2) throw new Error
           ("The puzzle must have at least two noun types!");
		this.maxNouns = this.nounTypes[0].nouns.length;
		if (this.maxNouns < 2) throw new Error
           ("The puzzle must have at least two nouns per type!");
		for (let nounType of this.nounTypes) {
			if (nounType.nouns.length !== this.maxNouns) 
            throw new Error("The puzzle must have the same number of nouns per type!");
		}
		//print("Validate the verbs.");
		this.verbs = [IsNot, Maybe, Is];
		if (this.verbs.length !== Verb.MaxVerbs) throw new Error
           ("The puzzle must have exactly " + Verb.MaxVerbs + " verbs!");
		this.maxLinks = this.links.length;
		//print("Validate the links. maxLinks=" + this.maxLinks);
		if (this.maxLinks < 1) throw new Error
           ("The puzzle must have be at least one link!");
		//print("Validate the first link (with)");
		if (this.links[0].nounType == null) this.links[0].nounType = this.nounTypes[0];
		//print("Validate every link has a noun type and a function. 
        //links=" + Q + this.links + Q);
		for (let link of this.links) {
			//print("link.nounType=" + Q + link.nounType + Q);
			if (link.nounType == null) {
				throw new Error("Link " + link.num + 
                                " must have a noun type!" + NL + link.name);
			}
			if (link.f == null) {
				throw new Error("Link " + link.num + 
                      " must have a function!" + NL + link.name);
			}
			link.update();
		}
		this.maxFacts = this.facts.length;
		//print("Validate the facts. maxFacts=" + this.maxFacts);
		for (let fact of this.facts) {
			if (fact.verb === Maybe) {
				throw new Error("Fact " + fact.num + 
                      " cannot use the possible verb!" + NL + fact.name);
			}
			if (fact.noun1 === fact.noun2) {
				throw new Error("Fact " + fact.num + 
                      " cannot have both nouns be the same!" + NL + fact.name);
			}
			let link = fact.link;
			let type = link.nounType;
			if (link.num < 1 && fact.noun1.type === fact.noun2.type) {
				throw new Error("Fact " + fact.num + 
                " cannot state that two nouns of the same type are 
                [not] together!" + NL + fact.name);
			}
			if (fact.noun1.type === type && fact.noun2.type === type) {
				throw new Error("Fact " + fact.num + " cannot have the link 
                      and both nouns with the same type!" + NL + fact.name);
			}
		}
		this.maxRules = this.rules.length;
		//print("Validate the rules. maxRules=" + this.maxRules);
		for (let rule of this.rules) {
			if (rule.f == null) {
				throw new Error("Rule " + rule.num + 
                      " must have a function!" + NL + rule.name);
			}
		}
		// Verify there is at least one fact or rule.
		if (this.facts.length < 1 && this.rules.length < 1) {
			throw new Error("The puzzle must have at least one fact or rule!");
		}
		// Initialize the sizes of the arrays for each noun.
		for (let nounType of this.nounTypes) {
			for (let noun of nounType.nouns) {
				noun.pairs = new Array(this.maxNounTypes);
				for (let fact of this.facts) {
					let link = fact.link;
					if (link.num < 1) continue;
					if (link.nounType === fact.noun1.type || 
                        link.nounType === fact.noun2.type) continue;
					if (fact.noun1 === noun || fact.noun2 === noun) {
						noun.facts.push(fact);
					}
				}
			}
		}
		// The puzzle is valid.
		this.isValid = true;
		
		//print("Calculate number of grids, then pairs, then marks.");
		this.maxGrids = this.maxNounTypes * (this.maxNounTypes - 1) / 2;
		this.maxPairs = this.maxGrids * this.maxNouns;
		this.maxMarks = this.maxPairs * this.maxNouns;
		//print("Initalize marks array where maxMarks=" + this.maxMarks);
		this.marks = new Array(this.maxMarks);
		for (let i = 0; i < this.maxMarks; i++) { this.marks[i] = new Mark(i); }
		//print("Initialize grids array where maxGrids=" + this.maxGrids);
		grids = new Array(this.maxGrids);
		for (let g = 0; g < this.maxGrids; g++) {
			grids[g] = new Array(this.maxNouns);
			for (let n1 = 0; n1 < this.maxNouns; n1++) {
				grids[g][n1] = new Array(this.maxNouns);
				for (let n2 = 0; n2 < this.maxNouns; n2++) {
					grids[g][n1][n2] = null;
				}
			}
		}
		this.resetWork();
		return 0;
	};
	// ---------------------------------------------------------------------------------
	/**
	 * Resets the puzzle data used for solving a puzzle.
	 * Called by validate, viewer.resetWork, solver,resetWork.
	 */
	this.resetWork = function () {
		//print("Reset name and pairs for each noun.");
		this.numPairs = 0;
		for (let nounType of this.nounTypes) { nounType.reset(); }
		
		//print("Reset facts.");
		this.numFacts = 0; this.numFactHits = 0;
		for (let fact of this.facts) { fact.reset(); }
		
		//print("Reset rules.");
		this.numRules = 0; this.numRuleHits = 0;
		for (let rule of this.rules) { rule.reset(); }
		//print("Reset marks.");
		this.numMarks = 0;
		for (let mark of this.marks) { mark.reset(); }
		
		this.numGuesses = 0;
		//print("Reset grids.");
		for (let g = 0; g < this.maxGrids; g++) {
			for (let n1 = 0; n1 < this.maxNouns; n1++) {
				for (let n2 = 0; n2 < this.maxNouns; n2++) {
					grids[g][n1][n2] = null;
				}
			}
		}
	};
	// ---------------------------------------------------------------------------------
	// Puzzle Answer.
	// ---------------------------------------------------------------------------------
	/** Puzzle answer. */
	this.answer = null;
	
	/**
	 * Determines if the current solution is correct.
	 * @returns {boolean} True if the solution is correct 
     * (or the puzzle's answer is null), otherwise false.
	 */
	this.isAnswer = function () {
		if (this.answer === null) return true;
		let nounType1 = this.nounTypes[0];
		for (let noun1 of nounType1.nouns) {
			for (let nounType2 of this.nounTypes) {
				if (nounType2.num === 1) continue;
				//print((Puzzle.getPairNounNum(noun1, nounType2) - 1) + 
                //' ' + this.answer[nounType2.num - 2][noun1.num - 1]);
				if ((Puzzle.getPairNounNum(noun1, nounType2) - 1) 
                   !== this.answer[nounType2.num - 2][noun1.num - 1]) return false;
			}
		}
		return true;
	};
	// --------------------------------------------------------------------------------
	// Always set the default names for the verbs. 
    // The verbs are initialized in the Verb class.
	IsNot.name = "is not";
	Maybe.name = "may be";
	Is.name = "is";
	// Always initialize the first link With.
	With = this.addLink("with", null);
	With.f = SmartLink.getIsWith();
	
	// ---------------------------------------------------------------------------------
	// Puzzle Solver.
	// ---------------------------------------------------------------------------------
	
	/** Number of facts examined by the Solver. */
	this.numFacts = 0;
	
	/** Number of times any fact has been referenced by the Solver. */
	this.numFactHits = 0;
	
	/** Number of rules examined by the Solver. */
	this.numRules = 0;
	
	/** Number of times any rule has been referenced by the Solver. */
	this.numRuleHits = 0;
	
	/** Number of marks entered by the Solver. */
	this.numMarks = 0;
	
	/** Number of positive marks entered by the Solver. */
	this.numPairs = 0;
	
	/** Number of assumptions made by the Solver. */
	this.numGuesses = 0;
	/** Maximum number of marks. */
	this.maxMarks = 0;
	
	/** Maximum number of pairs. */
	this.maxPairs = 0;
	
	/** Maximum number of grids. */
	this.maxGrids = 0;
	
	// -------------------------------------------------------------------------------
	// Marks.
	// -------------------------------------------------------------------------------
	
	/** Array of marks. */
	this.marks = [];
	
	/**
	 * Returns the last mark in the stack, or null.
	 * @returns {Mark|null} Mark object, or null.
	 */
	this.getLastMark = function () {
		return (this.numMarks > 0) ? this.marks[this.numMarks - 1] : null;
	};
	/**
	 * Returns the last mark entered by the user, or null. Called by undoUserMark.
	 * @returns {Mark|null} Mark object, or null.
	 */
	this.getLastUserMark = function () {
		for (let i = this.numMarks; i > 0; i--) {
			let mark = this.marks[i - 1];
			if (mark.type === Mark.Type.User) return mark;
		}
		return null;
	};
	
	/**
	 * Removes marks back to and including the last mark entered by the levels.
	 * @param {function|null} callback Callback function, or null.
	 */
	this.undoAssumption = function (callback = null) {
		while (this.numMarks > 0) {
			let mark = this.removeMark(callback);
			if (mark.type === Mark.Type.Level) break;
		}
	};
	
	/**
	 * Removes marks back to and including the last mark entered by the user.
	 * @param {function|null} callback Callback function, or null.
	 */
	this.undoUserMark = function (callback = null) {
		let mark = this.getLastUserMark();
		if (mark === null) return;
		print("puzzle.undoUserMark The last mark you entered is " + mark);
		while (this.numMarks > 0) {
			mark = this.removeMark(callback);
			if (mark.type === Mark.Type.User) break;
		}
	};
	/**
	 * Removes the last mark in the stack, and returns it. 
     * Called by undoUserMark, undoAssumption.
	 * @param {function|null} callback Callback function, or null.
	 * @returns {mark} Mark.
	 */
	this.removeMark = function (callback = null) {
		let mark = this.marks[this.numMarks - 1];
		//print("puzzle.removeMark " + mark.num);
		// Undo grids.
		this.removeGridMark(mark);
		
		// Undo pairs.
		if (mark.verb === Is) {
			--this.numPairs;
			mark.noun1.pairs[mark.noun2.type.num - 1] = null;
			mark.noun2.pairs[mark.noun1.type.num - 1] = null;
		}
		mark.clearPlaceholders();
		
		mark.undoDisabledFacts();
		
		--this.numMarks;
		if (callback !== null) callback(mark);
		//print("puzzle.removeMark " + mark.num);
		return mark;
	};
	// ---------------------------------------------------------------------------------
	
	/**
	 * Calls the addMark method with information from the object literal.
	 * @param {object} obj Object literal.
	 */
	this.addMarkByObjLit = function (obj) {
		let mc = (solver !== null) ? "WW" : "UI";
		//print(mc + " exec puzzle.addMarkByObjLit " + obj);
		let rs = 0;
		// Assume mark is by user if the type is not given.
		let markType = Mark.Type.User;
		if (obj.type != null) markType = Mark.getType(obj.type);
		//print("markType=" + Q + markType.name + Q);
		// Assume mark is an assumption if the level number is not given.
		let levelNum = obj.levelNum == null || 
                       obj.levelNum < 1 ? Solver.MaxLevels : obj.levelNum;
		let levelSub = obj.levelSub == null ? ' ' : obj.levelSub;
		let refNum = obj.refNum == null ? levelNum : obj.refNum;
		let refChar = obj.refChar == null ? levelSub : obj.refChar;
		let noun1 = this.getNoun(obj.noun1TypeNum, obj.noun1Num);
		let noun2 = this.getNoun(obj.noun2TypeNum, obj.noun2Num);
		let verb = this.getVerb(obj.verbNum);
		//print(Q + noun1 + Q + " " + Q + verb + Q + " " + Q + noun2 + Q);
		let reason = obj.reason == null ? "" : obj.reason;
		//print("reason=" + Q + reason + Q);
		let facts = [];
		if (obj.facts != null) {
			for (let inum of obj.facts) {
				facts.push(this.facts[inum - 1]);
			}
		}
		//print("facts=" + Q + facts + Q);
		let lonerNum = obj.lonerNum == null ? -1 : obj.lonerNum;
		let refMark = obj.refMark == null || 
                      obj.refMark === -1 ? null : this.marks[obj.refMark - 1];
		//print("refMark=" + Q + refMark + Q);
		
		let disabledFacts = [];
		if (obj.disabledFacts != null) {
			for (let inum of obj.disabledFacts) {
				disabledFacts.push(this.facts[inum - 1]);
			}
		}
		
		// If Solver received user mark as object literal, 
        // use the solver's addMark method.
		// If Viewer received any  mark as object literal, 
        // use the puzzle's addMark method.
		if (solver !== null)
			rs = solver.addMark(levelNum, levelSub, markType, 
                 refNum, refChar, noun1, verb, noun2, reason, 
                 facts, lonerNum, refMark);
		else
			rs = this.addMark(levelNum, levelSub, markType, 
                 refNum, refChar, noun1, verb, noun2, reason, 
                 facts, lonerNum, refMark, disabledFacts);
		
		//print(mc + " done puzzle.addMarkByObjLit numMarks=" + 
        //this.numMarks + " rs=" + rs);
		return rs;
	};
	// --------------------------------------------------------------------------------
	// WW for addMark
	// 1. Finder: If fact has been fully examined, the fact is disabled.
	// 2. Finder: A referenced fact is placed in the facts array.
	// 3. Solver: If mark (usually via rule or law) "covers" an enabled fact, 
    //    the fact is disabled and placed in the disabledFacts array
	// 4. All facts in the facts array update counters.
	// 5. All disabled facts in the facts array are COPIED to the disabledFacts array.
	
	// UI for addMark
	// 1. All facts in the facts array update counters.
	// 2. All facts in the disabledFacts array are disabled.
	
	
	/**
	 * Updates the mark in the array. Called by addMarkByUser, 
     * addMarkByRule, finder.addMark, lawyer.addMark.
	 * @param {number} levelNum Level number.
	 * @param {string} levelSub Level character.
	 * @param {Mark.Type} markType Mark type.
	 * @param {number} refNum Reference number.
	 * @param {string} refChar Reference character.
	 * @param {Noun} noun1 Noun 1.
	 * @param {Verb} verb Verb.
	 * @param {Noun} noun2 Noun 2.
	 * @param {string} reason Reason.
	 * @param {Array|null} facts Array of facts, or null
	 * @param {number} lonerNum Zero-based Loner number.
	 * @param {Mark|null} refMark Reference mark, or null.
	 * @param {Array|null} disabledFacts Array of facts, or null.
	 * @returns {Mark} Mark object.
	 */
	this.addMark = function (levelNum, levelSub, markType, refNum, refChar, noun1, 
	                         verb, noun2, reason, facts = null, lonerNum = -1, 
                             refMark = null, disabledFacts = null) {
		let mc = (solver !== null) ? "WW" : "UI";
		//print(mc + " addMark " + (this.numMarks + 1));
		
		if (markType === Mark.Type.User) levelNum = Solver.MaxLevels;
		// Update the mark, and increment the number of marks.
		let mark = this.marks[this.numMarks++];
		mark.type = markType;
		mark.reason = reason;
		mark.levelNum = levelNum;
		mark.levelSub = levelSub;
		mark.refNum = refNum;
		mark.refChar = refChar;
		mark.noun1 = noun1;
		mark.verb = verb;
		mark.noun2 = noun2;
		mark.facts = facts === null ? [] : facts;
		mark.lonerNum = lonerNum;
		mark.refMark = refMark;
		mark.disabledFacts = [];
		mark.guess = markType === Mark.Type.User || 
		(markType === Mark.Type.Level && levelNum === Solver.MaxLevels);
		if (mark.guess) ++this.numGuesses;
		
		// Update pairs.
		if (mark.verb === Is) {
			++this.numPairs;
			mark.noun1.pairs[mark.noun2.type.num - 1] = mark;
			mark.noun2.pairs[mark.noun1.type.num - 1] = mark;
		}
		
		// Update grids.
		this.setGridMark(mark);
		// The Solver must put disabled facts into the list.
		if (solver !== null) {
			// Copy disabled facts to the list.
			for (let fact of mark.facts) {
				if (!fact.enabled) mark.disabledFacts.push(fact);
			}
			// If mark (usually by rule or law) covers enabled fact of type 1, 
			// disable fact and add to disabled list.
			for (let fact of this.facts) {
				if (!fact.enabled || fact.type !== 1 || mark.verb !== fact.verb || 
				(mark.facts.length > 0 && mark.facts[0] === fact)) continue;
				if ((mark.noun1 === fact.noun1 && mark.noun2 === fact.noun2) || 
				(mark.noun1 === fact.noun2 && mark.noun2 === fact.noun1)) {
					fact.enabled = false;
					mark.disabledFacts.push(fact);
					//print("Mark " + mark.num + " disabled fact " + fact.num);
				}
			}
		}
		
		// The Viewer must disable the facts in the list.
		if (viewer !== null) {
			if (disabledFacts !== null) {
				for (let fact of disabledFacts) {
					fact.enabled = false;
				}
				mark.disabledFacts = disabledFacts;
			}
		}
		
		// Each fact in the facts array updates the counters.
		for (let fact of mark.facts) {
			++fact.hits;
			if (fact.hits === 1) ++this.numFacts;
			++this.numFactHits;
			if (viewer !== null) viewer.updateOnFact(fact);
		}
		// Update work variables and UI when a rule triggers a mark.
		// See sayRuleViolation when a mark violates a rule.
		if (mark.type === Mark.Type.Rule) {
			let ruleNum = mark.refNum;
			let rule = this.rules[ruleNum - 1];
			++rule.hits;
			if (rule.hits === 1) ++this.numRules;
			++this.numRuleHits;
			if (viewer) viewer.updateOnRule(rule);
		}
		
		if (viewer) viewer.updateOnMark(mark);
		//print("done puzzle.addMark");
		return mark;
	};
	// ----------------------------------------------------------------------------------
	// Grids. Answers the question "what is the mark (verb) for noun1 and noun2?"
	// ----------------------------------------------------------------------------------
	/** Array of grids. */
	let grids = [];
	/**
	 * Returns the mark in the grid given by two nouns.
	 * @param {Noun} noun1 Noun 1.
	 * @param {Noun} noun2 Noun 2.
	 * @returns {Mark|null} Mark object, or null.
	 */
	this.getGridMark = function (noun1, noun2) {
		return this.getGridMark2(noun1.type.num, noun1.num, noun2.type.num, noun2.num);
	};
	/**
	 * Returns the mark in the grid given by the one-based type number 
     * and number for two nouns.
	 * Called by getGridMark.
	 * @param {number} t1 One-based number of noun 1's type.
	 * @param {number} n1 One-based number of noun 1.
	 * @param {number} t2 One-based number of noun 2's type.
	 * @param {number} n2 One based number of noun 2.
	 * @returns {Mark|null} Mark object, or null.
	 */
	this.getGridMark2 = function(t1, n1, t2, n2) {
		if (t1 === t2) return null;
		if (t1 < t2) {
			let g = this.getGridNum(t1, t2);
			return grids[g - 1][n1 - 1][n2 - 1];
		}
		else {
			let g = this.getGridNum(t2, t1);
			return grids[g - 1][n2 - 1][n1 - 1];
		}
	};
	/**
	 * Returns the verb of the mark given by the two nouns, 
     * or the possible verb if the mark is null.
	 * Called by maybeRelated, finder, lawyer.
	 * @param {Noun} noun1 Noun 1.
	 * @param {Noun} noun2 Noun 2.
	 * @returns {Verb} Verb object.
	 */
	this.getGridVerb = function (noun1, noun2) {
		if (noun1.type === noun2.type) return IsNot;
		let mark = this.getGridMark(noun1, noun2);
		return (mark === null) ? Maybe : mark.verb;
	};
	/**
	 * Enters the given mark into the grids. Called by solver.addMark.
	 * @param {Mark} mark Mark object.
	 */
	this.setGridMark = function (mark) {
		this.setGridMark2(mark.noun1.type.num, mark.noun1.num, 
                          mark.noun2.type.num, mark.noun2.num, mark);
	};
	/**
	 * Removes the given mark from the grids. Called by solver.removeMark.
	 * @param {Mark} mark Mark object.
	 */
	this.removeGridMark = function (mark) {
		this.setGridMark2(mark.noun1.type.num, mark.noun1.num, 
                          mark.noun2.type.num, mark.noun2.num, null);
	};
	/**
	 * Enters the mark/null into the grids using the one-based numbers of two nouns.
	 * Called by setGridMark, removeGridMark.
	 * @param {number} t1 One-based number of noun 1's type.
	 * @param {number} n1 One-based number of noun 1.
	 * @param {number} t2 One-based number of noun 2's type.
	 * @param {number} n2 One-based number of noun 2.
	 * @param {Mark|null} mark Mark object, or null.
	 */
	this.setGridMark2 = function (t1, n1, t2, n2, mark) {
		if (t1 === t2) return;
		if (t1 < t2) {
			let g = this.getGridNum(t1, t2);
			grids[g - 1][n1 - 1][n2 - 1] = mark;
		}
		else {
			let g = this.getGridNum(t2, t1);
			grids[g - 1][n2 - 1][n1 - 1] = mark;
		}
	};
	/**
	 * Determines if the mark already exists.
	 * Called by solver.addMarkByRule, finder, lawyer.
	 * @param {Noun} noun1 Noun 1.
	 * @param {Verb} verb Verb.
	 * @param {Noun} noun2 Noun 2.
	 * @returns {Boolean} True if the mark already exists, otherwise false.
	 */
	this.isMark = function (noun1, verb, noun2) {
		let mark = this.getGridMark(noun1, noun2);
		let b = mark !== null && mark.verb === verb;
		//print("puzzle.isMark(" + noun1 + "," + verb + "," + noun2 + ")?" + b);
		return b;
	};
	/**
	 * Returns a list of nouns of noun type 2 that may be with noun 1. Called by ???
	 * @param {Noun} noun1 Noun 1.
	 * @param {NounType} nounType2 Noun type 2.
	 * @returns {Array} Array of nouns.
	 */
	this.getNouns = function (noun1, nounType2) {
		let nouns = [];
		for (let noun2 of nounType2.nouns) {
			if (this.getGridMark(noun1, noun2) === null) nouns.push(noun2);
		}
		return nouns;
	};
	/**
	 * Returns the one-based grid number given the one-based numbers of two noun types.
	 * Called by getGridMark2, setGridMark2.
	 * @param {number} t1 One-based number of noun type 1.
	 * @param {number} t2 One-based number of noun type 2.
	 * @returns {number} One-based number of the grid.
	 */
	this.getGridNum = function (t1, t2) {
		return (t1 < t2) ? (t1 - 1) * this.maxNounTypes + t2 - t1 * 
		(t1 + 1) / 2 : (t2 - 1) * this.maxNounTypes + t1 - t2 * (t2 + 1) / 2;
	};
	// ---------------------------------------------------------------------------------
	// Pairs. Answers the question "What mark paired noun1 with a noun of type2?" 
	// Mark may be null.
	// mark = noun1.pairs[t2 - 1]. If not null get mark.noun1 or 
    //        mark.noun2 that is not noun1
	// mark = noun2.pairs[t1 - 1]. If not null get mark.noun1 or 
    //        mark.noun2 that is not noun2
	// ----------------------------- ----------------------------------------------------
	/**
	 * Determines if noun 1 is/maybe related to noun 2. Called by NewSelfImprovement.
	 * @param {Noun} noun1 Noun 1.
	 * @param {Link} link Link.
	 * @param {Noun} noun2 Noun 2.
	 * @returns {boolean} True if noun 1 is or may be related to noun 2.
	 */
	this.maybeRelated = function (noun1, link, noun2) {
		let ok = false;
		let type = link.nounType;
		let slot1 = Puzzle.getPairNoun(noun1, type);
		let slot2 = Puzzle.getPairNoun(noun2, type);
		if (slot1 !== null && slot2 !== null) {
			// 1. Returns true if both nouns are slotted, and the slots are related.
			if (link.f(slot1, slot2) === Is) return true;
		}
		else if (slot1 !== null && slot2 === null) {
			// 2. Returns true if slot1 is related to any possible slot for noun2.
			for (let slotB of type.nouns) {
				if (this.getGridVerb(slotB, noun2) !== Maybe) continue;
				if (link.f(slot1, slotB) === Is) return true;
			}
		}
		else if (slot1 === null && slot2 !== null) {
			// 3. Returns true if any possible slot for noun1 is related to slot2.
			for (let slotA of type.nouns) {
				if (this.getGridVerb(slotA, noun1) !== Maybe) continue;
				if (link.f(slotA, slot2) === Is) return true;
			}
		}
		else {
			// 4. Returns true if any possible slot for noun1 is related to 
            //    any possible slot for noun2.
			for (let slotA of type.nouns) {
				if (this.getGridVerb(slotA, noun1) !== Maybe) continue;
				for (let slotB of type.nouns) {
					if (this.getGridVerb(slotB, noun2) !== Maybe) continue;
					if (link.f(slotA, slotB) === Is) return true;
				}
			}
		}
		return ok;
	};
	/**
	 * Determines if noun 1 can be with noun 2. Called by canBeLinked, getCommonNoun.
	 * @param {Noun} noun1 Noun 1.
	 * @param {Noun} noun2 Noun 2.
	 * @returns {boolean} True if noun 1 can be with noun 2, otherwise false.
	 */
	this.canBeWith = function (noun1, noun2) {
		let rs = false;
		let noun;
		// Return false if there is an 'X' for noun1 and noun2.
		let oldMark = this.getGridMark(noun1, noun2);
		if (oldMark !== null && oldMark.verb === IsNot) return rs;
		// Return false if noun1 is with another noun of noun2's type.
		noun = Puzzle.getPairNoun(noun1, noun2.type);
		if (noun !== null && noun !== noun2) return rs;
		// Return false if noun2 is with another noun of noun1's type.
		noun = Puzzle.getPairNoun(noun2, noun1.type);
		if (noun !== null && noun !== noun1) return rs;
		return true;
	};
	/**
	 * Determines if noun 1 can be related to slot 2 of the link's noun type.
	 * @param {Noun} noun1 Noun 1.
	 * @param {Link} link Link.
	 * @param {Noun} slot2 Slot 2.
	 * @param {number} i Number.
	 * @returns {boolean} True if noun 1 can be related to slot 2, otherwise false.
	 */
	this.canBeLinked = function (noun1, link, slot2, i) {
		let slots = link.nounType.nouns;
		for (let slot1 of slots) {
			let verb = (i !== 1) ? link.f(slot1, slot2) : link.f(slot2, slot1);
			if (verb === Is && this.canBeWith(slot1, noun1)) return true;
		}
		return false;
	};
	/**
	 * Returns the first noun of noun type 3 that noun 1 and noun 2 can be with, 
     * otherwise null.
	 * @param {Noun} noun1 Noun 1.
	 * @param {Noun} noun2 Noun 2.
	 * @param {NounType} nounType3 Noun type 3.
	 * @returns {Noun|null} Noun, or null
	 */
	this.getCommonNoun = function (noun1, noun2, nounType3) {
		for (let noun3 of nounType3.nouns) {
			if (this.canBeWith(noun1, noun3) && this.canBeWith(noun2, noun3)) 
                return noun3;
		}
		return null;
	};
}
/**
 * Returns noun 2 if noun 1 is with a noun of noun type 2, or null.
 * Called by Puzzle.isPair, maybeRelated, canBeWith.
 * @param {Noun} noun1 Noun 1.
 * @param {NounType} nounType2 Noun type 2.
 * @returns {Noun|null} Noun, or null.
 */
Puzzle.getPairNoun = function (noun1, nounType2) {
	let mark = noun1.pairs[nounType2.num - 1];
	if (mark === null) return null;
	return mark.noun1 === noun1 ? mark.noun2 : mark.noun1;
};
/**
 * Returns the one-based number of noun 2 
 * if noun 1 is with a noun of noun type 2, otherwise 0.
 * Called by Helper.getChartAsHtml
 * @param {Noun} noun1 Noun 1.
 * @param {NounType} nounType2 Noun type 2.
 * @returns {number} One-based number of noun 2.
 */
Puzzle.getPairNounNum = function (noun1, nounType2) {
	let mark = noun1.pairs[nounType2.num - 1];
	if (mark === null) return 0;
	return mark.noun1 === noun1 ? mark.noun2.num : mark.noun1.num;
};
/**
 * Determines if noun 1 is with noun 2. NOT USED
 * @param {Noun} noun1 Noun 1.
 * @param {Noun} noun2 Noun 2.
 * @returns {boolean} True if noun 1 is with noun 2, otherwise false.
 */
Puzzle.isPair = function (noun1, noun2) {
	return Puzzle.getPairNoun(noun1, noun2.type) === noun2 ? true : false;
};

谜题模块

谜题模块存储解决它所需的所有数据和函数。为此,该类必须继承自基类 Puzzle。下面是逻辑谜题 "五座房子" 的谜题模块。请注意,由于此谜题中的所有关系都可以用事实表示,因此没有规则。

FiveHouses 类

/* Puzzle module. Copyright by Michael Benson for Mystery Master. 2017-03-15. */
/* global IsNot, Is, With, SmartLink, Puzzle, puzzle */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";
function FiveHouses() {
	// Properties.
	this.myName = "FiveHouses";
	this.myTitle = "Five Houses";

	// Nouns.
	let houses = this.addNounType("House");
	let house1 = houses.addNoun("1st");
	let house2 = houses.addNoun("2nd");
	let house3 = houses.addNoun("3rd");
	let house4 = houses.addNoun("4th");
	let house5 = houses.addNoun("5th");

	let colors = this.addNounType("Color");
	let red = colors.addNoun("red");
	let green = colors.addNoun("green");
	let white = colors.addNoun("white");
	let yellow = colors.addNoun("yellow");
	let blue = colors.addNoun("blue");

	let nationalities = this.addNounType("Nationality");
	let englishman = nationalities.addNoun("Englishman");
	let spaniard = nationalities.addNoun("Spaniard");
	let ukrainian = nationalities.addNoun("Ukrainian");
	let norwegian = nationalities.addNoun("Norwegian");
	let japanese = nationalities.addNoun("Japanese man", "Japanese");

	let hobbies = this.addNounType("Hobby");
	let stamps = hobbies.addNoun("stamps");
	let antiques = hobbies.addNoun("antiques");
	let sings = hobbies.addNoun("singing");
	let gardens = hobbies.addNoun("gardening");
	let cooking = hobbies.addNoun("cooking");

	let pets = this.addNounType("Pet");
	let dogs = pets.addNoun("dogs");
	let snails = pets.addNoun("snails");
	let fox = pets.addNoun("fox");
	let horse = pets.addNoun("horse");
	let zebra = pets.addNoun("zebra");

	let drinks = this.addNounType("Drink");
	let coffee = drinks.addNoun("coffee");
	let tea = drinks.addNoun("tea");
	let milk = drinks.addNoun("milk");
	let juice = drinks.addNoun("juice");
	let water = drinks.addNoun("water");

	// Links.
	let directlyRightOf = this.addLink("directly to the right of", houses);
	directlyRightOf.f = SmartLink.getIsMoreBy(1);

	let nextTo = this.addLink("next to", houses);
	nextTo.f = SmartLink.getIsNextTo();

	// Facts.
	this.addFact("1", englishman, Is, With, red, 
	             "The Englishman lives in the red house.");
	this.addFact("2", spaniard, Is, With, dogs, 
	             "The Spaniard owns dogs.");
	this.addFact("3", coffee, Is, With, green, 
	             "Coffee is drunk in the green house.");
	this.addFact("4", ukrainian, Is, With, tea, 
	             "The Ukrainian drinks tea.");
	this.addFact("5", green, Is, directlyRightOf, white, 
	             "The green house is directly to the right of the white one.");
	this.addFact("6", stamps, Is, With, snails, 
	             "The stamp collector owns snails.");
	this.addFact("7", antiques, Is, With, yellow, 
	             "The antiques collector lives in the yellow house.");
	this.addFact("8", house3, Is, With, milk, 
	             "The man in the middle house drinks milk.");
	this.addFact("9", norwegian, Is, With, house1, 
	             "The Norwegian lives in the first house.");
	this.addFact("10", sings, Is, nextTo, fox, 
	             "The man who sings lives next to the man with the fox.");
	this.addFact("11", gardens, Is, With, juice, 
	             "The man who gardens drinks juice.");
	this.addFact("12", antiques, Is, nextTo, horse, 
	             "The antiques collector lives next to the man with the horse.");
	this.addFact("13", japanese, Is, With, cooking, 
	             "The Japanese man's hobby is cooking.");
	this.addFact("14", norwegian, Is, nextTo, blue, 
	             "The Norwegian lives next to the blue house.");

	// Solution.
	this.answer = [ [ 3, 4, 0, 2, 1 ], [ 3, 2, 0, 1, 4 ], 
	              [ 1, 2, 0, 3, 4 ], [ 2, 3, 1, 0, 4 ], [ 4, 1, 2, 3, 0 ] ];
}

FiveHouses.prototype = new Puzzle();
FiveHouses.prototype.constructor = FiveHouses;
puzzle = new FiveHouses();

原型继承

您可以看到谜题模块类的名称是 FiveHouses。那么,我们的子类如何继承 Puzzle 类的能力呢?每个谜题模块的最后三行执行基于原型的语言的“继承”。注意:变量 puzzle 之前已定义为全局变量。

 FiveHouses.prototype = new Puzzle();
 FiveHouses.prototype.constructor = FiveHouses;
 puzzle = new FiveHouses();

以下是每行代码的描述

  1. FiveHouses 类设置为继承自基类 Puzzle。不幸的是,子类 FiveHouses 的构造函数现在被设置为父类 Puzzle 的构造函数。
  2. FiveHouses 的构造函数设置为其自身的构造函数。
  3. 实例化(创建)一个 FiveHouses 类的对象,该对象继承自 Puzzle,并调用其自身的构造函数。

我目前不使用 ECMAScript 2015 中引入的 classconstructor 关键字。所以当我说构造函数时,我真正的意思是 FiveHouses 函数,我将其视为一个类。让我们讨论一下您在谜题模块中可能看到的每个部分。

属性

属性是逻辑谜题的元数据。我们唯一关心的属性是谜题的名称和标题。这些信息通过 Puzzle 成员 myNamemyTitle 设置。我使用这些名称而不是更通用的 nametitle,以避免命名冲突。

 // Properties.
 this.myName = "FiveHouses";
 this.myTitle = "Five Houses";

名词

逻辑谜题中的名词必须组织成类别。这些类别称为名词类型。一个谜题必须至少有两种名词类型。我们示例谜题的名词类型是:房子颜色国籍爱好宠物饮料。请注意,名词类型的名称是单数,而不是复数。对于这个谜题,每种类型有五个名词。下面是名词的表格,其中列标题是名词类型。请记住,每种名词类型必须有相同数量的名词,并且每种名词类型必须至少有两个名词。

名词
# 房子 Color 国籍 爱好 宠物 饮料
1 第1个 红色 英国人 邮票 咖啡
2 第2个 绿色 西班牙人 古董 蜗牛
3 第3个 白色 乌克兰人 唱歌 狐狸 牛奶
4 第4个 黄色 Norwegian 园艺 果汁
5 第5个 蓝色 Japanese 烹饪 斑马

此表的第一列(#)显示了名词的基于1的编号。通常,名词类型中名词的顺序并不重要,但有一个主要例外

如果链接引用名词类型,则该类型的名词必须按逻辑顺序排列。

阅读链接部分时,您会明白原因。

占位符

虽然大多数逻辑谜题会给出谜题中的所有名词,但有些非常困难的谜题可能不会给出所有名词的值。这意味着这些值必须通过一个或多个规则计算出来。初始值未知名词被称为占位符。通常这些值是数字,例如在“天体物理学会议”中参加讲座的人数,或者在“花花公子销售员”中销售员的年龄。

名词类型类

/* global Q, Helper, Noun */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";
/**
 * @class NounType class.
 * @description The Noun Type class.
 * @author Michael Benson <michael.benson@mysterymaster.com>
 * @copyright Mystery Master
 * @version 2017-03-27
 * @param {number} num One-based number.
 * @param {string} name Name.
 * @returns {NounType} Noun Type.
 */
function NounType(num, name) {
	/** One-based number of the noun type. */
	this.num = num + 1;
	
	/** Name of the noun type. */
	this.name = name;
	
	/** Array of nouns of this noun type. */
	this.nouns = [];
	this.toString = function () { return this.name; };
	this.asString = function () {
		return "num=" + Q + this.num + Q + " name=" + Q + 
                this.name + Q + " nouns=" + Helper.getArrayAsString(this.nouns);
	};
	/**
	 * Creates and appends the new noun to the nouns array of this noun type.
	 * @param {string} name Name.
	 * @param {string} title Title.
	 * @returns {Noun} Noun.
	 */
	this.addNoun = function (name, title) {
		let noun = new Noun(this.nouns.length, this, name, title);
		this.nouns.push(noun);
		return noun;
	};
	/**
	 * Returns the noun with the given one-based number of this noun type.
	 * @param {num} num One-based number of the noun.
	 * @returns {Noun} Noun.
	 */
	this.getNoun = function (num) {
		return this.nouns[num - 1];
	};
	/** Resets the nouns of this noun type. */
	this.reset = function () {
		for (let noun of this.nouns) noun.reset();
	};
}

名词类

/* global Q, Helper */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";
/**
 * @class Noun class.
 * @description The Noun class.
 * @author Michael Benson <michael.benson@mysterymaster.com>
 * @copyright Mystery Master
 * @version 2017-03-27
 * @param {number} num One-based number.
 * @param {NounType} type Noun Type.
 * @param {string} name Name.
 * @param {string} title Title.
 * @returns {Noun} Noun.
 */
function Noun(num, type, name, title = null) {
	/** One-based number of the noun. */
	this.num = num + 1;
	
	/** Noun type of the noun. */
	this.type = type;
	
	/** Name of the noun. This is used to create the name of the fact. */
	this.name = name;
	
	/**
	 * Title of the noun. Set to the first-capped name if not given. 
     * Displayed in the Nouns, Chart, and Grids forms.
	 * Note: This value is updated if the noun is a placeholder.
	 */
	this.title = title === null ? Helper.toTitleCase(name) : title;
	
	/** Mark = pairs[t2 - 1] for each noun type. Mark may be null. 
        Initialized in the validate method. */
	this.pairs = [];
	
	/** Facts that references this noun. Set in the validate method. */
	this.facts = [];
	let oldName = this.name;
	let oldTitle = this.title;
	this.toString = function () { return this.name; };
	this.asString = function () {
		return "num=" + Q + this.num + Q + " type=" + Q + this.type + Q + 
               " name=" + Q + this.name + Q + " title=" + Q + this.title + 
               Q + " oldName=" + Q + oldName + Q + " oldTitle=" + Q + oldTitle + 
               Q + " pairs.length=" + Q + this.pairs.length + Q;
	};
	/** Resets the name and title, along with the pairs. */
	this.reset = function () {
		this.resetValue();
		for (let i = 0; i < this.pairs.length; i++) {
			this.pairs[i] = null;
		}
	};
	/**
	 * Updates the noun if it is a placeholder.
	 * @param {string} value Value.
	 */
	this.updateValue = function (value) {
		//print("noun.updateValue oldTitle=" + 
        //Q + oldTitle + " value=" + Q + value + Q);
		this.name = value;
		this.title = value;
	};
	/** Resets the noun if it is a placeholder. */
	this.resetValue = function () {
		this.name = oldName;
		this.title = oldTitle;
	};
}

添加名词

名词类型使用 Puzzle 方法 addNounType 创建。每种名词类型名词使用 NounType 方法 addNoun 创建。以下是第一个名词类型 House 的代码。

 // Nouns.
 let houses = this.addNounType("House");
 let house1 = houses.addNoun("1st");
 let house2 = houses.addNoun("2nd");
 let house3 = houses.addNoun("3rd");
 let house4 = houses.addNoun("4th");
 let house5 = houses.addNoun("5th");

动词

一个逻辑谜题中总共有三个动词。这三个动词分别是:否定(假)动词、可能(未知)动词和肯定(真)动词。每个动词都有一个简短的文本短语作为其名称,一个字符作为其代码。我们的示例逻辑谜题有以下动词。

动词
# 类型 名称 代码
-1 负片 不是 X
0 可能 可能是  
1 正数 is O

以下是每个动词的简要描述。

  • 否定动词的名称是“is not”,其代码是“X”字符。
  • 可能动词的名称是“may be”,其代码是空白字符。
  • 肯定动词的名称是“is”,其代码是“O”字符。

动词名称的时态可以是现在时、过去时或将来时,并且只影响否定动词和肯定动词。一般来说,否定动词和肯定动词的名称应来自线索。下面是一个例子。

  • 否定动词可以是“is not”、“was not”或“will not be”。
  • 肯定动词可以是“is”、“was”或“will be”。

您在网格形式中看到的字符由每个动词的字符给出。在开始解决逻辑谜题之前,网格中的每个单元格都包含可能动词,通常由一个空白字符表示。要解决逻辑谜题,每个包含可能动词的单元格必须替换为否定动词('X')或肯定动词('O')。当所有单元格都正确填充后,您就得到了逻辑谜题的解决方案

动词类

/* global Q */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";
/**
 * @class Verb class.
 * @description The Verb class.
 * @author Michael Benson <michael.benson@mysterymaster.com>
 * @copyright Mystery Master
* @version 2017-05-10
 * @param {number} num Number.
 * @param {string} name Name.
 * @param {char} code Code.
 * @param {string} style CSS for the code.
 * @returns {Verb} Verb.
 */
function Verb(num, name, code, style = null) {
	/** Number of the verb, either -1, 0, or 1. */
	this.num = num;
	
	/** Type of the verb. */
	this.type = Verb.Types[num + 1];
	
	/** Name of the verb. */
	this.name = name;
	
	/** The code representing the verb in link tables and grids, 
        usually 'X', ' ', and 'O'. */
	this.code = code;
	if (style === null) {
		switch (this.num) {
			case -1: style = "color:red;"; break;
			case 1: style = "color:black;"; break;
		}
	}
	
	/** CSS style of the code. */
	this.style = style;
	this.toString = function () { return this.name; };
	this.asString = function () {
		return "num=" + Q + this.num + Q + " type=" + Q + this.type + 
               Q + " name=" + Q + this.name + Q + " code=" + Q + this.code + 
               Q + " style=" + Q + this.style + Q;
	};
	/**
	 * Returns the code with its CSS style.
	 * @returns {string} Verb code with CSS styling.
	 */
	this.getCode = function () {
		return "<span style=\"" + this.style + "\">" + this.code + "</span>";
	};
}
/** Verb types. */
Verb.Types = ["Negative", "Possible", "Positive"];
/** Maximum number of verbs. */
Verb.MaxVerbs = Verb.Types.length;
/** Negative verb. This is a global. */
let IsNot = new Verb(-1, "is not", "X");
/** Possible verb. This is a global. */
let Maybe = new Verb(0, "may be", " ");
/** Positive verb. This is a global. */
let Is = new Verb(1, "is", "O");

添加动词

对于每个谜题模块,总是定义三个名词。这三个全局变量是:IsNotMaybeIs。由于这些动词的预定义属性是可以接受的,因此我们的谜题模块不需要此部分。

注意:我知道我应该避免使用全局变量,但我也想避免在每个类成员前加上 "this"。所以这是我的折衷方案。

谜题中两个名词之间的所有关系都必须用动词和链接来表达。当你检查谜题中的线索,如果关系不明显,这意味着链接是“with”。例如,我们示例谜题中的第一条线索写道:“英国人住在红房子里。”这条线索可以表达为一个事实,其中第一个名词是“英国人”,动词是肯定的,链接是“with”。任何时候线索表明一个名词与另一个名词在一起或不在一起,只需使用默认链接“with”。换句话说,“住在”这个短语实际上意味着“with”。这个事实的数据可以解释为:fact(英国人, is, with, 红色)。

对于我们的示例谜题,有三个链接:“with”、“right of”和“next to”。所有链接都使用名词类型House。要理解这些链接,请画出这个谜题中五座房子的图片。您需要一张排成一行的五座编号房子的图片,其中第一座房子在最左边,第五座房子在最右边。假设每座房子最初都是无色的。

1st House 2nd House 3rd House 4th House 5th House
第1个 第2个 第3个 第4个 第5个

有了

我们逻辑谜题中的第一条线索是:“英国人住在红房子里。”如果一条线索说明一个名词与另一个名词在一起或不在一起,则使用默认链接“with”。用不那么出色的英语来说,第一条线索告诉我们“英国人与红房子在一起。”“with”链接是一种一对一关系,因为它意味着一个名词与自身在一起。此链接是为您自动定义的,并且默认设置为谜题的第一个名词类型。对于我们的谜题,以下是唯一为真的陈述

  • 1号房与1号房在一起
  • 2号房与2号房在一起
  • 3号房与3号房在一起
  • 4号房与4号房在一起
  • 5号房与5号房在一起

我强烈建议,如果一个名词类型被链接引用,那么它应该是在您定义时第一个名词类型。考虑到这一点,我应该指出,一个逻辑谜题可能有一些链接引用一个名词类型,而另一些链接引用另一个名词类型。例如,“小镇汽车旅馆”有七个链接引用三个不同的名词类型。那可真是一个困难的逻辑谜题!在这种情况下,让最合乎逻辑的名词类型排在第一位。

紧邻右侧

线索给出的第二个链接是“紧邻右侧”。此链接在线索5“绿房子紧邻白房子右侧”中。回到我们的图片,这意味着只有以下陈述为真。

  • 2号房在1号房的右边
  • 3号房在2号房的右边
  • 4号房在3号房的右边
  • 5号房在4号房的右边

这种类型的链接具有“一对一”关系,因为恰好一栋房子位于另一栋房子的右侧。

相邻

线索给出的第三个链接是“相邻”。这个链接出现在线索10、12和14中。虽然有些房子只与一栋房子相邻,但其他房子位于两栋房子之间。因此,这个链接不是“一对一”的。你能确定这个链接的所有真实陈述吗?

比较

“紧邻右侧”链接是“大于”比较,因为我们想看名词A是否高于名词B。这种比较是使用名词的基于1的编号完成的。为了减少逻辑谜题中的链接数量,请使用“大于”或“小于”比较,但不要同时使用两者。例如,如果一个事实陈述“A小于B”,你也可以说“B大于A”,反之亦然。下面是我们的示例谜题的链接。第一个表格显示了链接。以下是每列的简要描述。

  1. # 是链接的基于零的编号。
  2. 名词类型是链接引用的名词类型。
  3. 名称是链接的名称。
  4. 1:1 告诉我们链接是否是一对一的(选中)或不是(未选中)。

随后的表格显示了每个链接的链接网格。这种网格以视觉方式告知我们名词 A(最左侧的行标题列)和名词 B(最顶部的列标题行)之间的关系。如果交叉单元格中有一个“O”,则名词 A 和名词 B 之间的关系为真。如果交叉单元格中有一个“X”,则名词 A 和名词 B 之间的关系为假。

链接
# 名词类型 名称 1:1
0 房子
1 房子 紧邻右侧
2 房子 相邻  
房子 第1个 第2个 第3个 第4个 第5个
第1个 O X X X X
第2个 X O X X X
第3个 X X O X X
第4个 X X X O X
第5个 X X X X O
紧邻右侧
房子 第1个 第2个 第3个 第4个 第5个
第1个 X X X X X
第2个 O X X X X
第3个 X O X X X
第4个 X X O X X
第5个 X X X O X
相邻
房子 第1个 第2个 第3个 第4个 第5个
第1个 X O X X X
第2个 O X O X X
第3个 X O X O X
第4个 X X O X O
第5个 X X X O X

链接类

/* global Q, IsNot, Is */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";
/**
 * @class Link class.
 * @description The Link class.
 * @author Michael Benson <michael.benson@mysterymaster.com>
 * @copyright Mystery Master
 * @version 2017-03-27
 * @param {number} num Zero-based number.
 * @param {string} name Name.
 * @param {NounType} nounType Noun Type.
 * @returns {Link} Link.
 */
function Link(num, name, nounType) {
	/** Zero-based number of the link. */
	this.num = num;
	
	/** Name of the link. */
	this.name = name;
	
	/** Noun Type of the link. */
	this.nounType = nounType;
	
	/** True if function is one-to-one (at most one positive verb per row). 
        Set in the validate method. */
	this.oneToOne = false;
	
	/** Function that returns either negative or positive verb given 
        two nouns of the link's noun type. */
	this.f = null;
	// Depending on the verb for a type 4 fact, indicates if two nouns 
    // can be in the same slot.
	let ssNeg = true;
	let ssPos = true;
	this.toString = function () { return this.name; };
	this.asString = function () {
		return "num=" + Q + this.num + Q + " name=" + Q + this.name + 
               Q + " nounType=" + Q + this.nounType + Q + " oneToOne=" + 
               Q + this.oneToOne + Q + " ssNeg=" + Q + ssNeg + Q + 
               " ssPos=" + Q + ssPos + Q;
	};
	/** TODO */
	this.update = function () {
		this.oneToOne = isOneToOne(this);
		ssNeg = inSameSlot(this, IsNot);
		ssPos = inSameSlot(this, Is);
	};
	/**
	 * Returns true if two nouns can be in the same slot for this link.
	 * @param {Verb} verb Verb.
	 * @returns {boolean} True if two nouns can be in the same slot, otherwise false.
	 */
	this.canBeWith = function (verb) {
		return verb.num < 0 ? ssNeg : ssPos;
	};
	/**
	 * Returns true if the link is one-to-one, otherwise false.
	 * @param {Link} link Link.
	 * @returns {boolean} True if the link is one-to-one, otherwise false.
	 */
	function	isOneToOne(link) {
		let flag = false;
		let slots = link.nounType.nouns;
		for (let slot1 of slots) {
			let cnt = 0;
			for (let slot2 of slots) {
				let verb = link.f(slot1, slot2);
				if (verb.num === 1 && ++cnt > 1) return flag;
			}
		}
		flag = true;
		return flag;
	}
	// Example 1 facts: A is [not] higher than B.
	// Positive: Returns false since they cannot share any slot.
	// Negative: Returns true since they can share a slot.
	// Example 2 facts: A is [not] on the same side as B.
	// Positive: Returns true since they can share any slot.
	// Negative: Returns false.
	/**
	 * Returns true if A and B can be in the same slot given the link and verb.
	 * If false, we can quickly say A is not with B for a fact of type 2 or 4.
	 * @param {Link} link Link.
	 * @param {Verb} verb Verb.
	 * @returns {boolean} True if two nouns can be in the same slot, otherwise false.
	 */
	function inSameSlot(link, verb) {
		let slots = link.nounType.nouns;
		for (let slot of slots) {
			if (link.f(slot, slot) === verb) return true;
		}
		return false;
	}
}
/** Default link. This is a global variable set in the Puzzle class. 
    Unlike the global verbs, With is dependent on the puzzle's nouns. */
let With = null;

添加链接

第一个链接“with”被定义为全局变量,因此它始终可用于每个谜题模块。每个链接都通过 Puzzle 方法 addLink 创建。以下是我们谜题中其他链接的代码。

 let directlyRightOf = this.addLink("directly to the right of", houses);
 directlyRightOf.f = SmartLink.getIsMoreBy(1);
 let nextTo = this.addLink("next to", houses);
 nextTo.f = SmartLink.getIsNextTo();

注意:通过 f 成员分配给每个链接的函数由 SmartLink 静态类中的方法提供。有关更多信息,请参阅 智能链接 部分。如果您在 SmartLink 类中找不到您想要的内容,则需要“自己动手”。

事实

事实是两个名词之间的静态关系。例如,“A 在 B 旁边。”事实具有以下形式。

“名词 1 动词 链接 名词 2。”其中 (1) 动词是肯定或否定,(2) 动词和链接是两个名词之间的关系。

我们示例谜题中的第一条线索可以直接表达为一个事实:“英国人住在红房子里。”事实上(双关语),这个谜题的独特之处在于每条线索都是一个事实。以下是这个谜题的事实。

事实
# X 命中 名称
1 0 英国人在红房子里(线索 1)。
2 0 西班牙人的宠物是狗(线索 2)。
3 0 绿房子里喝咖啡(线索 3)。
4 0 乌克兰人最喜欢的饮料是茶(线索 4)。
5 0 绿房子紧邻白房子的右侧(线索 5)。
6 0 爱好是集邮的男人养蜗牛(线索 6)。
7 0 爱好是古董的男人住在黄房子里(线索 7)。
8 0 3号房的男人喝牛奶(线索 8)。
9 0 挪威人在1号房里(线索 9)。
10 0 爱好是唱歌的男人在养狐狸的男人旁边(线索 10)。
11 0 爱好是园艺的男人喝果汁(线索 11)。
12 0 爱好是古董的男人在养马的男人旁边(线索 12)。
13 0 日本男人的爱好是烹饪(线索 13)。
14 0 挪威人靠近蓝房子(线索 14)。

以下是每列的简要描述。

  1. # 是事实的基于 1 的编号。
  2. X 告诉您事实是否已启用(选中)或已禁用(未选中)。
  3. 命中数是事实被引用的次数。
  4. 名称是事实的文本。

事实的类型

事实的类型如下所示。虽然第一种类型的事实只产生一个标记,但其他类型的事实通常产生多个标记。

类型 1

类型 1 事实具有默认链接“with”。只有类型 1 事实将“with”作为链接。它最容易处理,也最容易在标记与其矛盾时被注意到。我们示例谜题“五座房子”中的大多数事实都是类型 1 事实。

我必须指出,类型 1 事实中的两个名词不能具有相同的名词类型。为什么?因为在任何逻辑网格谜题中,相同名词类型的两个名词永远不能在一起!如果你有一个包含两个名字的事实,例如“Abe 和 Bob 在一起”,这将是一个违规。如果你有一个事实“Abe 和 Bob 不在一起”,那只是多余的。

类型 2

类型 2 事实只有一个名词,其名词类型与链接的名词类型匹配。它处理起来稍微困难一些,并且在标记与其矛盾时更难捕捉到。这个谜题,以及大多数谜题,都没有这种类型的事实。

一个确实包含类型2事实的逻辑谜题是“幸运街道”。在这个谜题中,事实15指出“她比星期五更早找到硬币。”这意味着硬币不是在星期五或星期六找到的。硬币的名词类型是“Coin”,而链接“earlier than”和星期五的名词类型都是“Day”。在这个谜题中,事实16和17也是类型2事实。

我再次强调,类型 2 事实中的两个名词不能具有相同的名词类型。为什么?因为这就像说“星期四比星期五早。”虽然这可能是事实,但这已经在“earlier than”链接中定义了。

类型 3

类型 3 事实的两个名词具有相同的名词类型,但此名词类型与链接的名词类型不同。我们示例谜题中的类型 3 事实是事实 5:“绿房子紧邻白房子的右侧。”绿房子和白房子具有名词类型“颜色”,而链接“紧邻右侧”具有名词类型“房子”。

类型 4

类型 4 事实是指两个名词和链接的名词类型都不同。从我们的示例谜题中,事实 10、12 和 14 是类型 4 事实。让我们检查事实 10:“唱歌的人住在养狐狸的人旁边。”唱歌的人的名词类型是“爱好”。链接“旁边”的名词类型是“房子”。养狐狸的人的名词类型是“宠物”。

关于逻辑谜题的一个重要问题是线索中给出的两个对象是否不同,因此不能配对。对于类型 1 事实,答案是明确给出的。对于其他类型的事实,通常链接会让你知道这是否属实。如果一个事实陈述:“Abe 在一个房间里,这个房间旁边是猫所在的房间”,那么你就知道 Abe 永远不能和猫在同一个房间里。

但是,如果事实陈述:“Abe 在一个在猫所在房间旁边的房间里”,那么 Abe 可能和猫在同一个房间里。我的建议是,如果线索没有另行说明,则假定线索中给出的两个名词不同的。

事实类

/* global Q */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";
/**
 * @class Fact class.
 * @description The Fact class.
 * @author Michael Benson <michael.benson@mysterymaster.com>
 * @copyright Mystery Master
 * @version 2017-03-27
 * @param {number} num One-based number.
 * @param {string} name Name.
 * @param {Noun} noun1 Noun 1.
 * @param {Verb} verb Verb.
 * @param {Link} link Link.
 * @param {Noun} noun2 Noun 2.
 * @param {boolean} initEnabled Initial/reset value of the enabled field.
 * @returns {Fact} Fact.
 */
function Fact(num, name, noun1, verb, link, noun2, initEnabled = true) {
	/** One-based number of the fact. */
	this.num = num + 1;
	
	/** Type of the fact. Either 1, 2, 3, or 4. */
	this.type = 0;
	
	/** Name of the fact. */
	this.name = name;
	
	/** Noun 1 of the fact. */
	this.noun1 = noun1;
	
	/** Verb of the fact. */
	this.verb = verb;
	
	/** Link of the fact. */
	this.link = link;
	
	/** Noun 2 of the fact. */
	this.noun2 = noun2;
	
	/** Initial/reset value of the enabled field. */
	this.enabled = initEnabled === true;
	
	/** Number of times the fact was referenced by the Solver. */
	this.hits = 0;
	
	/** Initial/reset value of the enabled field. */
	this.initEnabled = this.enabled;
	if (link.num === 0)
		this.type = 1;
	else if (noun1.type === link.nounType || noun2.type === link.nounType)
		this.type = 2;
	else if (noun1.type === noun2.type)
		this.type = 3;
	else if (this.noun1.type !== noun2.type)
		this.type = 4;
	this.toString = function () { return this.name; };
	this.asString = function () {
		return "num=" + Q + this.num + Q + " type=" + Q + this.type + 
                        Q + " noun1=" + Q + this.name + Q + " noun1=" + 
                        Q + this.noun1 + Q + " verb=" + Q + this.verb + 
                        Q + " link=" + Q + this.link + Q + " noun2=" + 
                        Q + this.noun2 + Q + " enabled=" + Q + this.enabled + 
                        Q + " hits=" + Q + this.hits + Q + " initEnabled=" + 
                        Q + this.initEnabled + Q;
	};
	/** Resets the fact. */
	this.reset = function () {
		this.enabled = this.initEnabled;
		this.hits = 0;
	};
	/**
	 * Returns the message that the fact is being examined.
	 * @returns {string} Message.
	 */
	this.msgBasedOn = function () { return "fact " + this.num; };
	
	/**
	 * Returns the message that the fact is disabled.
	 * @returns {string} Message.
	 */
	this.msgDisabled = function () { return "fact " + this.num + " is disabled."; };
}

添加事实

这个逻辑谜题的独特之处在于,每条线索都恰好对应一个事实。这通常不是这样的!每个事实都通过 Puzzle 方法 addFact 创建。这是第一个事实。

 this.addFact("1", englishman, Is, With, red, "The Englishman lives in the red house.");
 

注意:如您在此事实中看到的,变量 IsWith 不需要以 this 关键字作为前缀,因为它们是全局变量。我希望“this”不是一个问题。

这种方法看起来非常简单。但我必须告诉您,这种方法具有重载,您一次可以输入多个事实。因此,让我们在另一篇文章中讨论这些重载是什么。

验证事实

在神秘大师解决逻辑谜题之前,它首先通过寻找各种逻辑错误来验证逻辑谜题。验证谜题的 Puzzle 方法被恰当地命名为 validate。以下是事实无效的一些原因。

  1. 事实的动词是可能动词(“maybe”)。它必须是肯定动词(“is”)或否定动词(“is not”)。
  2. 事实中的两个名词相同。事实中的名词必须不同。
  3. 事实的链接是“with”,但两个名词类型相同。这就像说“Bob不是Abe”,或者“Abe是Bob”。对于逻辑网格谜题,相同类型的两个名词无论如何都不会在一起。
  4. 事实的链接不是“with”,但两个名词的类型与链接的类型相同。例如,“排队中的第2个人在第4个人前面”可能有意义,但这是一种关系,而不是事实!这个陈述正是“在……前面”链接应该定义的。

规则

规则是两个或多个名词之间的条件关系,例如“如果 A 紧邻 B,那么 C 不紧邻 D。”当事实无法表示逻辑谜题中的线索时,就需要规则。由于我们的示例谜题没有规则,下面是谜题“筋疲力尽”的谜题模块。

/* Puzzle module. Copyright by Michael Benson for Mystery Master. 2017-02-25. */
function AllTiredOut() {
	"use strict";

	// Properties.
	this.myName = "AllTiredOut";
	this.myTitle = "All Tired Out";

	// Nouns.
	let slots = this.addNounType("Order");
	let slot1 = slots.addNoun("1st");
	let slot2 = slots.addNoun("2nd");
	let slot3 = slots.addNoun("3rd");
	let slot4 = slots.addNoun("4th");
	let slot5 = slots.addNoun("5th");

	let names = this.addNounType("Customer");
	let ethan = names.addNoun("Ethan");
	let grace = names.addNoun("Grace");
	let jeff = names.addNoun("Jeff");
	let lisa = names.addNoun("Lisa");
	let marge = names.addNoun("Marge");

	let wants = this.addNounType("Wanted");
	let alignment = wants.addNoun("alignment");
	let chains = wants.addNoun("chains");
	let jack = wants.addNoun("jack");
	let shocks = wants.addNoun("shock absorbers", "Shocks");
	let tires = wants.addNoun("tires");

	// Verbs.
	IsNot.name = "was not";
	Is.name = "was";

	// Links.
	let justAhead = this.addLink("just ahead of", slots);
	justAhead.f = SmartLink.getIsLessBy(1);

	let threeAhead = this.addLink("three places ahead of", slots);
	threeAhead.f = SmartLink.getIsLessBy(3);

	let nextTo = this.addLink("next to", slots);
	nextTo.f = SmartLink.getIsNextTo();

	// Facts
	this.sayFact = (noun1, verb, link, noun2) => {
		let msg = noun1.name + " " + verb.name + " " + link.name + " " + noun2.name;
		let lname = link === With ? " " : " " + link.name + " ";

		// Types: 1=Order, 2=Customer, 3=Wanted.
		switch (noun1.type.num) {
			case 1:
				msg = "The " + noun1.name + " person in line ";
				switch (noun2.type.num) {
					case 1: break;
					case 2:
						msg += verb.name + lname + noun2.name;
						break;
					case 3:
						msg += (verb === Is ? "did" : "did not") + 
                                " buy the " + noun2.name;
						break;
				}
				break;
			case 2:
				msg = noun1.name + " ";
				switch (noun2.type.num) {
					case 1:
						msg += verb.name + lname + "the " + 
                               noun2.name + " person in line";
						break;
					case 2: break;
					case 3:
						if (link === With)
							msg += (verb === Is ? "did" : "did not") + 
                                   " buy the " + noun2.name;
						else
							msg += verb.name + " " + link.name + 
                                   " the person who bought the " + noun2.name;
						break;
				}
				break;
			case 3:
				msg = "The person who bought the " + noun1.name + 
                      " " + verb.name + lname;
				switch (noun2.type.num) {
					case 1: break;
					case 2:
						msg += noun2.name;
						break;
					case 3:
						msg += "the one who bought the " + noun2.name;
						break;
				}
				break;
		}
		return msg + ".";
	};

	this.addFact("1", [ethan, slot3, chains], IsNot, With);
	this.addFact("2", jack, Is, justAhead, lisa);
	this.addFact("3", slot2, IsNot, With, [ ethan, jeff ]);
	this.addFact("4", tires, Is, threeAhead, alignment);
	this.addFact("6", jeff, Is, justAhead, shocks);

	// Rules.
	let rule1 = this.addRule("5", "Marge wasn't the second of the three women in line.");
	rule1.f = SmartRule.getIsNotBetween(this, rule1, slots, marge, grace, lisa);

	let rule2 = this.addRule("7", "Grace stood next to at least one man in line.");
	rule2.f = SmartRule.getIsRelated(this, rule2, grace, nextTo, [ethan, jeff]);

	// Solution.
	this.answer = [ [ 4, 1, 2, 3, 0 ], [ 1, 4, 2, 3, 0 ] ];
}

AllTiredOut.prototype = new Puzzle();
AllTiredOut.prototype.constructor = AllTiredOut;
puzzle = new AllTiredOut();

在这个谜题模块中,您可以看到我将动词的名称设置为过去时。

 // Verbs.
 IsNot.name = "was not";
 Is.name = "was";

对于这个逻辑谜题,线索 5 和 7 需要表示为规则。以下是这个谜题的规则。

规则
# X 命中 名称
1 0 玛吉不是排队的三个女人中的第二个(线索 5)。
2 0 格蕾丝至少与一个男人排在一起(线索 7)。

以下是每列的简要描述

  1. # 是规则的基于 1 的编号。
  2. X 告诉您规则是启用(选中)还是禁用(未选中)。
  3. 命中数是规则被引用的次数。
  4. 名称是规则的文本。

规则类

/* global Q, Helper */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";

/**
 * @class Rule class.
 * @description The Rule class.
 * @author Michael Benson <michael.benson@mysterymaster.com>
 * @copyright Mystery Master
 * @version 2017-03-27
 * @param {number} num One-based number of the rule.
 * @param {string} name Name.
 * @param {Array} nouns Array of nouns.
 * @param {boolean} initEnabled
 * @returns {Rule} Initial/reset value of the enabled field.
 */
function Rule(num, name, nouns = null, initEnabled = true) {
	/** One-based number of the rule. */
	this.num = num + 1;
	
	/** Name of the rule. */
	this.name = name;
	
	/** Optional array of nouns referenced in the rule. */
	this.nouns = nouns === null ? [] : nouns;
	
	/** Whether the rule should be referenced (true) or ignored (false). */
	this.enabled = initEnabled === true;
	
	/** Number of times the rule has been referenced. */
	this.hits = 0;
	
	/** Initial/reset value of the enabled field. */
	this.initEnabled = this.enabled;
	
	/** Function that checks for rule violations and/or triggers marks to be entered. */
	this.f = null;

	this.toString = function () { return this.name; };
	this.asString = function () {
		return "num=" + Q + this.num + Q + " name=" + Q + this.name + 
                Q + " nouns=" + Helper.getArrayAsString(this.nouns) + 
               " enabled=" + Q + this.enabled + Q + " hits=" + Q + 
                this.hits + Q + " initEnabled=" + Q + this.initEnabled + Q;
	};

	/** Resets the rule. */
	this.reset = function () {
		this.enabled = this.initEnabled;
		this.hits = 0;
	};
}

添加规则

以下是满足线索 5 和 7 的规则。

 // Rules.
 let rule1 = this.addRule("5", "Marge wasn't the second of the three women in line.");
 rule1.f = SmartRule.getIsNotBetween(this, rule1, slots, marge, grace, lisa);
 let rule2 = this.addRule("7", "Grace stood next to at least one man in line.");
 rule2.f = SmartRule.getIsRelated(this, rule2, grace, nextTo, [ethan, jeff]);

规则和链接一样,都需要编程。每个规则的函数 fSmartRule 静态类中的方法提供。有关更多信息,请参阅 智能规则 部分。规则通常根据标记执行操作,并返回状态码。状态码为负数表示违规,为零表示成功。规则可以执行以下一个或多个任务。

强制违规

当标记与谜题线索矛盾时,就会发生违规。与线索矛盾的标记称为矛盾。这只应在进行假设时发生。违规意味着程序应撤销上次假设,并进行另一次假设。如果未进行假设,则这是一个致命的逻辑错误,程序将停止解决谜题。在谜题“筋疲力尽”中,如果一个标记创建了格蕾丝第一、一个女人第二的情况,则此标记将与线索矛盾。当规则遇到这种情况时,它必须通知程序违规。

触发标记

一个检查当前标记以输入额外标记的规则称为触发器。触发器的状态是提交标记的状态。如果标记没有问题,状态为零。如果标记有问题,状态为负数,并立即返回。如果触发器输入的标记被拒绝,则与规则违规相同。如果触发器成功,则当前规则可以继续,并且可以调用其他规则。

对于逻辑谜题“筋疲力尽”,规则必须寻找这样的情况:“如果没有男人可以排第二,那么格蕾丝就不能排第一。”当发现这种情况时,规则将为“第一”和格蕾丝输入“X”。一般来说,规则违规应首先处理,其次是触发器。

更新占位符

有些逻辑谜题可能不会给出所有名词的值。这意味着这些值必须通过规则计算。初始值未知名词被称为占位符。通常这些值是数字,例如在“天体物理学会议”中参加讲座的人数,或者在“花花公子销售员”中销售员的年龄。更新占位符的规则可能相当复杂。

总而言之,规则是特定于逻辑谜题的定律。解决逻辑谜题的定律将在未来的文章中讨论。

解决方案

这是逻辑谜题的编码解决方案,但它是可选的。如果您知道解决方案,您可以在此处进行编码。看看您是否可以“解码”它。

 // Solution.
 this.answer = [ [ 3, 4, 0, 2, 1 ], [ 3, 2, 0, 1, 4 ], 
               [ 1, 2, 0, 3, 4 ], [ 2, 3, 1, 0, 4 ], [ 4, 1, 2, 3, 0 ] ];
 

助手

Helper 静态类定义了全局变量和有用的方法。我的几个类都引用了这个类。虽然不鼓励使用全局变量和方法,但下面是此模块中定义的一些全局变量。

  • Q 是一个字符串形式的双引号字符。
  • NL 是由转义序列 "\n" 给出的换行符。
  • print(msg) 是一个将给定消息打印到浏览器控制台窗口的方法。

Helper 类

/* global Puzzle */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";

// Globals.
const Q = "\"";
const NL = "\n";

/** My thread name is either "UI" or WW". */
let MTN = "";

function print(msg) {
	console.log(MTN + msg);
}

/**
 * @class Helper class.
 * @description The Helper class for common methods related to 
 * the Mystery Master application.
 * @author Michael Benson <michael.benson@mysterymaster.com>
 * @copyright Mystery Master
 * @version 2017-06-15
 */
function Helper() {
	throw new Error("Helper is a static class!");
}

// ------------------------------------------------------------------------------------
// Boolean Methods.

/**
 * Returns true if (a) val is boolean and true, (b) val is numeric and not zero, 
 * or (c) val is the string "true" (case insensitive), otherwise returns false.
 * @param {boolean} val Value
 * @returns {boolean} Boolean value.
 */
Helper.getBoolean = function (val = false) {
	if (typeof val === "boolean") return val;
	if (Helper.isNumber(val)) return val !== 0;
	if (typeof val !== "string") return false;
	val = val.toLowerCase();
	return val === "true";
};

// --------------------------------------------------------------------------------------
// Numeric Methods.

/**
 * Returns true if the given value is a number, otherwise false.
 * @param {type} val Value.
 * @returns {boolean} True if the value is a number, otherwise false.
 */
Helper.isNumber = function (val) {
	return typeof val === "number" && !isNaN(val);
};

/**
 * Returns the integer value of the given string.
 * @param {string} str String value.
 * @returns {number} Integer value.
 */
Helper.toInt = function (str = null) {
	return str === null ? 0 : parseInt(str);
};

/**
 * Return true if the value of the given string is an integer, otherwise false.
 * @param {string} str String value.
 * @returns {boolean} True if the string value is an integer, otherwise false.
 */
Helper.isInt = function (str = null) {
	if (str === null) return false;

	let val = typeof str === 'string' ? parseInt(str) : str;

	// Check if val is NaN.
	if (val !== val) return false;

	return parseFloat(val) === parseInt(val);
};

/**
 * Returns true if the integer value of the noun's name is not divisible by 
 * the given number, otherwise false.
 * @param {Noun} noun Noun.
 * @param {number} num Integer value.
 * @returns {boolean} True if the integer value of the noun's name 
 * is not divisible by the number, otherwise false.
 */
Helper.isNotDivisibleBy = function (noun = null, num) {
	if (noun === null || !Helper.isInt(noun.name)) return false;
	let val = Helper.toInt(noun.name);
	return val % num !== 0;
};

// --------------------------------------------------------------------------------------
// Date/Time Methods.

/**
 * Returns the current time as a formatted string.
 * @returns {string} Formatted string.
 */
Helper.getTimestamp = function () {
	let date = new Date();
	return new Date(date.getTime()).toLocaleString() + " " + date.getMilliseconds();
};

/**
 * Returns the given date as a string with format "YYYY-MM-DD hh:mm:ss.ms".
 * @param {Date} date Date value.
 * @returns {string} Formatted string.
 */
Helper.formatDT = function (date) {
	let yy = date.getFullYear();
	let mm = date.getMonth() + 1;
	let dd = date.getDate();
	let hh = date.getHours();
	let mi = date.getMinutes();
	let ss = date.getSeconds();
	let ms = date.getMilliseconds();
	let msg = "" + yy + "-" + (mm <= 9 ? "0" + mm : mm) + "-" + 
        (dd <= 9 ? "0" + dd : dd) +
		" " + (hh <= 9 ? "0" + hh : hh) + ":" + (mi <= 9 ? "0" + mi : mi) +
		":" + (ss <= 9 ? "0" + ss : ss) + "." + (ms <= 9 ? "00" + 
        ms : (ms <= 99 ? "0" + ms : ms));
	return msg;
};

/**
 * Returns the elapsed time between two times.
 * @param {Date} time1 Time 1.
 * @param {Date} time2 Time 2.
 * @returns {string} Message.
 */
Helper.getMsgElapsedTime = function (time1 = null, time2 = null) {
	if (time1 === null || time2 === null) return "";
	let elapsedTime = time2.getTime() - time1.getTime();
	return "" + elapsedTime + " ms.";
};

// --------------------------------------------------------------------------------------
// String Methods.

/**
 * Returns the message when the Solver starts solving.
 * @param {Date} time1 Time 1.
 * @returns {string} Message.
 */
Helper.getStartedMsg = function (time1) {
	return "I started solving at " + Helper.formatDT(time1) + ".";
};

/**
 * Returns the message when the Solver stops solving.
 * @param {Date} time1 Time 1.
 * @param {Date} time2 Time 2.
 * @returns {string} Message.
 */
Helper.getStoppedMsg = function (time1, time2) {
	return "I stopped solving at " + Helper.formatDT(time2) + 
    " in " + Helper.getMsgElapsedTime(time1, time2);
};

/**
 * Returns the message when the Solver has a solution.
 * @param {Date} time1 Time 1.
 * @param {Date} time2 Time 2.
 * @param {number} numSolutions Number of solutions.
 * @returns {string} Message.
 */
Helper.getSolutionMsg = function (time1, time2, numSolutions) {
	return "I have " + (numSolutions === 1 ? 
    "a solution" : numSolutions + " solutions") + " at " + 
    Helper.formatDT(time2) + " in " + Helper.getMsgElapsedTime(time1, time2);
};

/**
 * Returns a multi-line string as one line with new lines replaced by the given string.
 * @param {string} msg Multi-line message.
 * @param {string} sep Separator.
 * @returns {string} String.
 */
Helper.getMsgAsOneLine = function (msg, sep = " ") {
	return msg.replace(NL, sep);
};

/**
 * Returns the given string converted to title case.
 * @param {string} str String value.
 * @returns {string} String.
 */
Helper.toTitleCase = function (str = null) {
	if (str === null) return str;
	let n = str.length;
	if (n === 0) return str;

	let seps = " \t-";
	let flag = true;
	let chars = Array.from(str);
	for (let i = 0; i < n; i++) {
		if (seps.indexOf(chars[i]) > -1)
			flag = true;
		else if (flag === true) {
			chars[i] = chars[i].toUpperCase();
			flag = false;
		}
	}
	let res = chars.join("");
	return res;
};

/**
 * Returns the array of objects as a comma-delimited string.
 * @param {Object[]} objs Array of objects.
 * @param {string} sep0 Separator.
 * @returns {string} String.
 */
Helper.getArrayAsString = function (objs, sep0 = ",") {
	let msg = "[", sep = "";
	for (let obj of objs) {
		msg += sep + obj.toString();
		sep = sep0;
	}
	return msg + "]";
};

// -----------------------------------------------------------------------------------
// Matrix Methods. Note: For referencing a 2D array, C#: a[i,j], JavaScript: a[i][j].

/**
 * Prints the 2D array.
 * @param {Array} a 2D array of objects.
 */
Helper.sayArray2D = function (a = null) {
	let msg = "";
	if (a === null) return msg;
	for (let i1 = 0; i1 < a.length; i1++) {
		let line = "", sep = "";
		for (let i2 = 0; i2 < a[i1].length; i2++) {
			line += sep + a[i1][i2];
			sep = ",";
		}
		msg += line + NL;
	}
	print(msg);
};

/**
 * Returns a 2-dimensional array with each element initialized to the given value.
 * @param {number} d1 Size of dimension 1.
 * @param {number} d2 Size of dimension 2.
 * @param {type} v Initial value.
 * @returns {Array} Initialized 2D array of objects.
 */
Helper.getArray2D = function (d1, d2, v) {
	if (v === undefined) v = 0;
	let a = new Array(d1);
	for (let i1 = 0; i1 < d1; i1++) {
		a[i1] = new Array(d2);
		for (let i2 = 0; i2 < d2; i2++) {
			a[i1][i2] = v;
		}
	}
	return a;
};

/**
 * Solves system of n linear equations with n variables x[0], 
 * x[1], ..., x[n-1] using Gaussian Elimination and Backward Substitution.
 * Note: This is an augmented matrix, so there is an additional column; 
 * hence, n is one less than the number of columns.
 * @param {Array} a Augmented 2D array.
 * @returns {Array} Array of x values.
 */
Helper.solveEquations = function (a) {
	let n = a[0].length - 1;
	//print("Helper.solveEquations n=" + n);
	let x = new Array(n);

	for (let i = 0; i < n - 1; i++) {
		// Search for row p where A[p, i] is not zero.
		let p = 0; // swap row index
		for (p = i; p < n; p++) {
			if (a[p][i] !== 0) break;
		}
		// No unique solution exists
		if (p === n) return x;

		if (p !== i) {
			// Swap rows.
			for (let c = 0; c < n + 1; c++) {
				let m = a[p][c];
				a[p][c] = a[i][c];
				a[i][c] = m;
			}
		}

		// Gaussian Elimination.
		for (let j = i + 1; j < n; j++) {
			let m = a[j][i] / a[i][i];
			for (let c = 0; c < n + 1; c++) {
				a[j][c] = a[j][c] - m * a[i][c];
			}
		}
	}

	// No unique solution exists
	if (a[n - 1][n - 1] === 0) return x;

	// Backward Substitution.
	x[n - 1] = a[n - 1][n] / a[n - 1][n - 1];
	for (let i = n - 2; i >= 0; i--) {
		let s = 0.0;
		for (let j = i + 1; j < n; j++) {
			s += a[i][j] * x[j];
		}
		x[i] = (a[i][n] - s) / a[i][i];
	}

	return x;
};

// -------------------------------------------------------------------------------------
// Text Methods. The HTML methods are in the Puzzler static class.

/**
 * Returns the chart as an text table.
 * @param {Puzzle} puzzle Puzzle object.
 * @param {number} chartCol1 Number of first column to display.
 * @param {boolean} isSolution Solution flag.
 * @returns {string} String.
 */
Helper.getChartAsText = function (puzzle, chartCol1, isSolution) {
	let txt = "";
	if (puzzle === null) return txt;

	let caption = isSolution ? "Solution" : "Chart";
	txt = caption + NL;

	let t = chartCol1;
	let nounTypes = puzzle.nounTypes;
	let nounType1 = nounTypes[t];

	let maxNounTypes = nounTypes.length;
	let maxNouns = nounType1.nouns.length;

	let w = 20;
	let pad = " ".repeat(w);
	let tmp;

	let i = 0, j = 0, k = 0;
	for (j = 0; j < maxNounTypes; j++) {
		if (k === t)++k;
		let nounType = (j === 0 ? nounType1 : nounTypes[k++]);
		tmp = nounType.name + pad;
		txt += tmp.substring(0, w);
	}
	txt += NL;
	for (i = 0; i < maxNouns; i++) {
		k = 0;
		for (j = 0; j < maxNounTypes; j++) {
			if (k === t) ++k;
			let noun1 = nounType1.nouns[i];
			tmp = " ";
			if (j === 0)
				tmp =noun1.title;
			else {
				let noun2 = Puzzle.getPairNoun(noun1, nounTypes[k]);
				if (noun2 !== null) tmp = noun2.title;
				++k;
			}
			tmp += pad;
			txt += tmp.substring(0, w);
		}
		txt += NL;
	}
	return txt;
};

SmartLink 静态类定义了返回谜题模块中链接的函数的方法。

智能链接类

/* global IsNot, Is */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";

/**
 * @class SmartLink class.
 * @description The SmartLink static class.
 * @author Michael Benson <michael.benson@mysterymaster.com>
 * @copyright Mystery Master
 * @version 2017-03-27
 */
function SmartLink() {
	throw new Error("SmartLink is a static class!");
}

/**
 * Returns positive verb if both nouns are equal (i.e., are the same noun), 
 * otherwise negative verb.
 * @returns {Function} Function isWith.
 */
SmartLink.getIsWith = function () {
	return (noun1, noun2) => noun1.num === noun2.num ? Is : IsNot;
};

/**
 * Returns positive verb if the number for noun1 is less than 
 * the number for noun2 minus n, otherwise negative verb.
 * For n = 1, this means "before, but not just before."
 * @param {number} n Number.
 * @returns {Function} Function isLessThan.
 */
SmartLink.getIsLessThan = function (n = 0) {
	return (noun1, noun2) => noun1.num < noun2.num - n ? Is : IsNot;
};

/**
 * Returns positive verb if the number for noun1 is exactly n less than 
 * the number for noun2, otherwise negative verb.
 * @param {number} n Number.
 * @returns {Function} Function isLessBy.
 */
SmartLink.getIsLessBy = function (n) {
	return (noun1, noun2) => noun1.num === noun2.num - n ? Is : IsNot;
};

/**
 * Returns positive verb if the number for noun1 is more than 
 * the number for noun2 plus n, otherwise negative verb.
 * For n = 1, this means "after, but not just after."
 * @param {number} n Number.
 * @returns {Function} Function isMoreThan.
 */
SmartLink.getIsMoreThan = function (n = 0) {
	return (noun1, noun2) => noun1.num > noun2.num + n ? Is : IsNot;
};

/**
 * Returns positive verb if the number for noun1 is exactly n more than 
 * the number for noun2, otherwise negative verb.
 * @param {number} n Number.
 * @returns {Function} Function isMoreBy.
 */
SmartLink.getIsMoreBy = function (n) {
	return (noun1, noun2) => noun1.num === noun2.num + n ? Is : IsNot;
};

/**
 * Returns positive verb if the number for noun1 is exactly 1 less 
 * or 1 more than the number for noun2, otherwise negative verb.
 * @returns {Function} Function isNextTo.
 */
SmartLink.getIsNextTo = function () {
	return (noun1, noun2) => (noun1.num === noun2.num - 1) || 
           (noun1.num === noun2.num + 1) ? Is : IsNot;
};

/**
 * Returns positive verb if the number for noun1 is exactly n less than 
 * or n more than the number for noun2, otherwise negative verb.
 * Equivalent to isNextTo when n is one.
 * @param {number} n Number.
 * @returns {Function} Function isOffsetBy.
 */
SmartLink.getIsOffsetBy = function (n) {
	return (noun1, noun2) => (noun1.num === noun2.num - n) || 
           (noun1.num === noun2.num + n) ? Is : IsNot;
};

/**
 * Returns positive verb if the number for noun1 is either n less than 
 * or n more than the number for noun2, otherwise negative verb.
 * @param {number} n Number.
 * @returns {Function} Function isOutsideOf.
 */
SmartLink.getIsOutsideOf = function (n) {
	return (noun1, noun2) => (noun1.num < noun2.num - n) || 
           (noun1.num > noun2.num + n) ? Is : IsNot;
};

/**
 * Returns positive verb if the number for noun1 times n1 
 * equals the number for noun2 times n2, otherwise negative verb.
 * @param {number} n1 Number on left hand side.
 * @param {number} n2 Number on right hand side.
 * @returns {Function} Function hasRatio.
 */
SmartLink.getHasRatio = function (n1, n2) {
	return (noun1, noun2) => (n1 * noun1.num === n2 * noun2.num) ? Is : IsNot;
};

智能规则

SmartRule 静态类定义了返回谜题模块中规则的函数的方法。

智能规则类

/* global Helper, IsNot, Maybe, Is, Puzzle, solver */
/* jshint unused:true, eqnull:true, esversion:6 */
"use strict";

/**
 * @class SmartRule class.
 * @description The SmartRule static class.
 * @author Michael Benson <michael.benson@mysterymaster.com>
 * @copyright Mystery Master
 * @version 2017-03-27
 */
function SmartRule() {
	throw new Error("SmartRule is a static class!");
}

// NOTE: puzzle is only needed for getMatchAtLeastOne.

// ---------------------------------------------------------------------------------
// 1. matchExactlyOne NOT IMPLEMENTED.

// ---------------------------------------------------------------------------------
// 2. matchAtLeastOne

/**
 * Returns the matchAtLeastOne function to enforce rule 
 * where noun1 is with at least one noun in nouns2.
 * See puzzles: AtTheAlterAltar, DogDuty, ModernNovels, PsychicPhoneFriends.
 * @param {Puzzle} puzzle Puzzle object.
 * @param {Rule} rule Rule.
 * @param {Noun} noun1 Noun 1.
 * @param {Noun} nouns2 Noun 2.
 * @returns {Function} Function matchAtLeastOne.
 */
SmartRule.getMatchAtLeastOne = function (puzzle, rule, noun1, nouns2) {

	// Returns true if noun1 can be with at least one noun in list2.
	function canBeWith2(noun1, nouns2) {
		for (let noun2 of nouns2) {
			if (noun1.type === noun2.type) continue;
			if (Puzzle.getPairNounNum(noun1, noun2.type) === noun2.num) return true;
			if (puzzle.canBeWith(noun1, noun2)) return true;
		}
		return false;
	}

	// Returns noun from list2 if it is the only noun that can be with noun1, 
    // otherwise null.
	function isOnlyNoun(noun1, nouns2) {
		let noun = null;
		for (let noun2 of nouns2) {
			if (noun1.type === noun2.type) continue;
			if (Puzzle.getPairNoun(noun1, noun2.type) === noun2) return null;
			if (!puzzle.canBeWith(noun1, noun2)) continue;
			if (noun !== null) return null;
			noun = noun2;
		}
		return noun;
	}

	function matchAtLeastOne(mark) {
		let rs = 0;

		// Violation if noun1 cannot be with any noun in nouns2.
		if (!canBeWith2(noun1, nouns2)) return -1;

		// Trigger if noun1 can only be with one noun in nouns2.
		let noun2 = isOnlyNoun(noun1, nouns2);
		if (noun2 !== null) {
			let msg = noun1.name + " must be with " + noun2.name + ".";
			rs = solver.addMarkByRule(mark, rule, ' ', noun1, Is, noun2, msg);
		}

		// Example: For "Dog Duty", Whiley belongs to a woman.
		// If Whiley can be with nounX, but no woman can be with nounX, 
        // then Whiley is not with nounX.
		// TODO Do this for other SmartRules? PsychicPhoneFriends benefits.
		for (let nounType of puzzle.nounTypes) {
			if (noun1.type === nounType) continue;
			for (let nounX of nounType.nouns) {
				if (puzzle.getGridVerb(noun1, nounX) === IsNot) continue;
				let ok = false;
				for (let noun of nouns2) {
					if (noun.type === nounType || puzzle.getGridVerb(noun, nounX) 
                        !== IsNot) {
						ok = true;
						break;
					}
				}
				if (!ok) {
					let msg = "SmartRule.matchAtLeastOne: 
                               No item in list can be with " + nounX.name + ".";
					//print(msg);
					rs = solver.addMarkByRule
                         (mark, rule, ' ', noun1, IsNot, nounX, msg);
					if (rs !== 0) return rs;
				}
			}
		}
		return rs;
	}
	return matchAtLeastOne;
};

// -------------------------------------------------------------------------------------
// 3. matchOneToExactlyOne

/**
 * Returns the matchOneToExactlyOne function to enforce rule where 
 * exactly one noun in nouns1 is with exactly one noun in nouns2.
 * See puzzles: ModernNovels.
 * @param {Puzzle} puzzle Puzzle object.
 * @param {Rule} rule Rule.
 * @param {Array} nouns1 Array of nouns.
 * @param {Array} nouns2 Array of nouns.
 * @returns {Function} Function matchOneToExactlyOne.
 */
SmartRule.getMatchOneToExactlyOne = function (puzzle, rule, nouns1, nouns2) {

	// Returns the number of matches (zero or more) between the nouns in both lists.
	function getNumMatches(nouns1, nouns2) {
		let cnt = 0;
		for (let noun1 of nouns1) {
			for (let noun2 of nouns2) {
				if (noun2.type === noun1.type) continue;
				if (Puzzle.isPair(noun1, noun2)) ++cnt;
			}
		}
		return cnt;
	}

	function matchOneToExactlyOne(mark) {
		let rs = 0;

		// Example: ModernNovels has exactly one of the two men 
        // (Oscar, Peter) chose a Faulkner novel 
        // ("Light in August", "Absalom! Absalom!").
		// If  only noun1 in list1 can be with noun2 in list2,
		// and only noun2 in list2 can be with noun1, then noun1 must be with noun2.
		// Also, there is a rule violation if all the counts are zero.

		// Get number of nouns in list1, list2.
		let n1 = nouns1.length;
		let n2 = nouns2.length;
		let counts = new Array(n1);

		let scanFlag = true;
		let i1 = -1; // index of noun1 with count of one, and all others zero.
		let i2 = -1; // index of noun2 that can be with noun1.

		// Examine each noun in list1.
		for (let i = 0; i < n1; i++) {
			let noun1 = nouns1[i];
			counts[i] = 0;

			// Examine each noun in list2.
			for (let j = 0; j < n2; j++) {
				let noun2 = nouns2[j];
				// Ignore noun2 if it has the same type as noun1.
				if (noun2.type === noun1.type) continue;

				// Abort if noun1 is already with noun2.
				if (Puzzle.isPair(noun1, noun2)) {
					scanFlag = false;
					break;
				}

				// Remember index of noun2 if noun1 can be with noun2.
				if (puzzle.canBeWith(noun1, noun2)) {
					// Abort if count is more than one.
					if (++counts[i] > 1) {
						scanFlag = false;
						break;
					}
					i2 = j;
				}
			}

			if (!scanFlag) break;
			// Remember index of noun1 if count is one.
			if (counts[i] === 1) {
				// Abort if more than one noun1 has a count of one.
				if (i1 !== -1) {
					scanFlag = false;
					break;
				}
				i1 = i;
			}
		}

		if (scanFlag) {
			if (i1 !== -1 && i2 !== -1) {
				// There is only one noun1 that can be with noun2.
				let noun1 = nouns1[i1];
				let noun2 = nouns2[i2];
				let msg = noun1.name + " must be with " + noun2.name + ".";
				rs = solver.addMarkByRule(mark, rule, ' ', noun1, Is, noun2, msg);
				if (rs !== 0) return rs;
			}
			else {
				// If all the counts are zero, then this is a rule violation.
				for (let i = 0; i < n1; i++) {
					if (counts[i] !== 0) {
						scanFlag = false;
						break;
					}
				}
				if (scanFlag) return -1;
			}
		}

		// Rule violation if the number of matches between nouns 
        // in list1 and list2 is more than one.
		if (getNumMatches(nouns1, nouns2) > 1) return -1;

		return rs;
	}
	return matchOneToExactlyOne;
};

// --------------------------------------------------------------------------------
// 4. matchOneToOne

/**
 * Returns the matchOneToOne function to enforce rule where each noun in 
 * nouns1 is uniquely matched with one noun in nouns2.
 * See puzzles: ModernNovels, SmallTownMotels.
 * @param {Puzzle} puzzle Puzzle object.
 * @param {Rule} rule Rule.
 * @param {Array} nouns1 Array of nouns.
 * @param {Array} nouns2 Array of nouns.
 * @returns {Function} Function matchOneToOne.
 */
SmartRule.getMatchOneToOne = function (puzzle, rule, nouns1, nouns2) {
	let listLength = nouns1.length;
	let grid = Helper.getArray2D(listLength, listLength, null);

	function matchOneToOne(mark) {
		let rs = 0;

		// Populate the grid with the current marks. 
        // Enter 'X' if both nouns have the same type.
		for (let row = 0; row < nouns1.length; row++) {
			let noun1 = nouns1[row];
			for (let col = 0; col < nouns2.length; col++) {
				let noun2 = nouns2[col];
				grid[row][col] = puzzle.getGridVerb(noun1, noun2);
				if (noun1.type === noun2.type) grid[row][col] = IsNot;
			}
		}

		// a) Rule violation if there is more than one 'O' per row 
        //    (may not happen too often).
		// Trigger: If a row has one 'O', enter 'X' for the other cols in that row.
		for (let row = 0; row < nouns1.length; row++) {
			let noun1 = nouns1[row];
			let cnt = 0;
			for (let col = 0; col < nouns2.length; col++) {
				if (grid[row][col] === Is)++cnt;
			}
			if (cnt > 1) {
				//print("DBG SmartGrid a) Too many positive marks in row!");
				return 1;
			}
			if (cnt === 1) {
				for (let col = 0; col < nouns2.length; col++) {
					let noun2 = nouns2[col];
					if (grid[row][col] !== Maybe) continue;
					let msg = "Only one of each noun in list2 
                               can be with one of each noun in list1.";
					//print(msg);
					rs = solver.addMarkByRule
                         (mark, rule, 'a', noun1, IsNot, noun2, msg);
					if (rs !== 0) return rs;
					grid[row][col] = IsNot;
				}
			}
		}

		// b) Rule violation if there is more than one 'O' per col 
        //    (may not happen too often).
		// Trigger: If a col has one 'O', enter 'X' for the other rows in that col.
		for (let col = 0; col < nouns2.length; col++) {
			let noun2 = nouns2[col];
			let cnt = 0;
			for (let row = 0; row < nouns1.length; row++) {
				if (grid[row][col] === Is) ++cnt;
			}
			if (cnt > 1) {
				//print("DBG SmartGrid b) Too many positive marks in col!");
				return 1;
			}
			if (cnt === 1) {
				for (let row = 0; row < nouns1.length; row++) {
					let noun1 = nouns1[row];
					if (grid[row][col] !== Maybe) continue;
					let msg = "Only one of each noun in list1 
                               can be with one of each noun in list2.";
					//print(msg);
					rs = solver.addMarkByRule
                         (mark, rule, 'b', noun1, IsNot, noun2, msg);
					if (rs !== 0) return rs;
					grid[row][col] = IsNot;
				}
			}
		}

		// c) Rule violation if there is all 'X' in the row (may not happen too often).
		// Trigger: If a row has all 'X' except one '?', enter 'O' for the '?'.
		for (let row = 0; row < nouns1.length; row++) {
			let noun1 = nouns1[row];
			let i = -1;
			let cnts = [0, 0, 0];
			for (let col = 0; col < nouns2.length; col++) {
				let verb = grid[row][col];
				cnts[verb.num + 1] += 1;
				if (verb.num === 0) i = col;
			}
			if (cnts[0] === listLength) {
				//print("DBG SmartGrid c) All negative marks in row!");
				return 1;
			}
			if (cnts[0] === listLength - 1 && cnts[1] === 1 && cnts[2] === 0) {
				let noun2 = nouns2[i];
				let msg = "Only one noun in list2 is available for noun1.";
				//print(msg);
				rs = solver.addMarkByRule(mark, rule, 'c', noun1, Is, noun2, msg);
				if (rs !== 0) return rs;
				grid[row][i] = Is;
			}
		}

		// d) Rule violation if there is all 'X' in the col (may not happen too often).
		// Trigger: if a col has all 'X' except one '?', enter 'O' for the '?'.
		for (let col = 0; col < nouns2.length; col++) {
			let noun2 = nouns2[col];
			let i = -1;
			let cnts = [0, 0, 0];
			for (let row = 0; row < nouns1.length; row++) {
				let verb = grid[row][col];
				cnts[verb.num + 1] += 1;
				if (verb.num === 0) i = row;
			}
			if (cnts[0] === listLength) {
				//print("DBG SmartGrid d) All negative marks in col!");
				return 1;
			}
			if (cnts[0] === listLength - 1 && cnts[1] === 1 && cnts[2] === 0) {
				let noun1 = nouns1[i];
				let msg = "Only one noun in list1 is available for noun2.";
				//print(msg);
				rs = solver.addMarkByRule(mark, rule, 'd', noun1, Is, noun2, msg);
				if (rs !== 0) return rs;
				grid[i][col] = Is;
			}
		}

		//printGrid();
		return rs;
	}
	return matchOneToOne;
};

// -----------------------------------------------------------------------------------
// 5. matchOneList

/**
 * Returns the matchOneList function to enforce the rule where the nouns in 
 * nouns1 must be with one list of nouns in array2.
 * See puzzles: Overdue, PlayingCards.
 * @param {Puzzle} puzzle Puzzle object.
 * @param {Rule} rule Rule.
 * @param {Array} nouns1 Array of nouns.
 * @param {Array} array2 2D array of nouns.
 * @returns {Function} Function matchOneList.
 */
SmartRule.getMatchOneList = function (puzzle, rule, nouns1, array2) {

	// Returns the zero-based index of the list that the given noun is in, otherwise -1.
	function getListIndex(nounX, lists) {
		let rs = -1;
		let idx = rs;
		for (let list of lists) {
			++idx;
			for (let noun of list) {
				if (noun === nounX) return idx;
			}
		}
		return rs;
	}

	// Returns true if there is coverage (or nothing to do), 
    // otherwise false for no coverage.
	function hasCoverage(nouns1, nouns2) {
		let rs = true;
		let n = nouns1.length;

		// Find unique nouns in nouns2 that can be with the nouns in nouns1.
		let nouns = [];
		let nbad = 0;
		for (let noun1 of nouns1) {
			let cnt = 0;
			for (let noun2 of nouns2) {
				let verb = puzzle.getGridVerb(noun1, noun2);
				if (verb === Is) return rs;
				if (verb === IsNot) continue;
				++cnt;
				if (!nouns.includes(noun2)) nouns.push(noun2);
			}
			if (cnt === 0) ++nbad;
		}

		rs = (nouns.length === 0 || nbad === n) || (nouns.length >= n && nbad === 0);
		return rs;
	}

	function matchOneList(mark) {
		let rs = 0;

		// Trigger if a noun1 is with a noun2 in one of the lists of array2, 
        // then the other nouns in nouns1 are not with any nouns in the other lists.
		// Example: If Wicks is with a Wednesday, then Jones is not with a Thursday.
		if (mark.verb === Is) {
			let nounX1 = null;
			let nounX2 = null;
			let idx2 = -1;
			for (let noun of nouns1) {
				if (mark.noun1 === noun) {
					nounX1 = mark.noun1;
					nounX2 = mark.noun2;
					idx2 = getListIndex(nounX2, array2);
				}
				else if (mark.noun2 === noun) {
					nounX1 = mark.noun2;
					nounX2 = mark.noun1;
					idx2 = getListIndex(nounX2, array2);
				}
				if (idx2 > -1) break;
			}

			// The other nouns in nouns1 are not with any nouns in the other lists.
			if (idx2 > -1) {
				//print("matchOneList: noun1 " + nounX1 + " is in list[" + idx2 + "].");
				let idx = -1;
				for (let list2 of array2) {
					if (++idx === idx2) continue;
					for (let noun2 of list2) {
						if (noun2 === nounX2) continue;
						for (let noun1 of nouns1) {
							if (noun1 === nounX1) continue;
							let msg = noun1.name + " is not with " + noun2.name + ".";
							rs = solver.addMarkByRule(mark, rule, 'a', 
                                        noun1, IsNot, noun2, msg);
							if (rs !== 0) return rs;
						}
					}
				}
			}
		}

		// Trigger for each nouns2 in array2, if there are not enough nouns 
        // in nouns2 to cover nouns1, then the nouns in nouns2 are not with 
        // the nouns in nouns1.
		for (let nouns2 of array2) {
			if (hasCoverage(nouns1, nouns2)) continue;
			for (let noun1 of nouns1) {
				for (let noun2 of nouns2) {
					let msg = noun1.name + " is not with " + noun2.name + ".";
					rs = solver.addMarkByRule(mark, rule, 'a', noun1, IsNot, noun2, msg);
					if (rs !== 0) return rs;
				}
			}
		}
		return rs;
	}
	return matchOneList;
};

// ------------------------------------------------------------------------------------
// 6. isNotBetween

/**
 * Returns the isNotBetween function to enforce the rule where noun1 is not 
 * between noun2 and noun3, where any two nouns may be slots.
 * Assumes the slots are ordered by number (either low to high or high to low). 
 * See puzzles: AllTiredOut.
 * @param {Puzzle} puzzle Puzzle object.
 * @param {Rule} rule Rule.
 * @param {NounType} nounType Noun type.
 * @param {Noun} noun1 Noun 1.
 * @param {Noun} noun2 Noun 2.
 * @param {Noun} noun3 Noun 3.
 * @returns {Function} Function isNotBetween.
 */
SmartRule.getIsNotBetween = function (puzzle, rule, nounType, noun1, noun2, noun3) {
	function isNotBetween(mark) {
		//print("isNotBetween mark=" + mark.num + " nounType=" + 
        //nounType.num + " noun1=" + Q + noun1 + Q + " noun2=" + 
        //Q + noun2 + Q + " noun3=" + Q + noun3 + Q);
		let rs = 0;

		// Use one-based numbers for each slot.
		let slotA = (noun1.type === nounType) ? 
                     noun1.num : Puzzle.getPairNounNum(noun1, nounType);
		let slotB = (noun2.type === nounType) ? 
                     noun2.num : Puzzle.getPairNounNum(noun2, nounType);
		let slotC = (noun3.type === nounType) ? 
                     noun3.num : Puzzle.getPairNounNum(noun3, nounType);

		// Violation if nounA is between nounB and nounC.
		if (slotA > 0 && slotB > 0 && slotC > 0) {
			if (slotB < slotA && slotA < slotC) return -1;
			if (slotC < slotA && slotA < slotB) return -1;
			return rs;
		}

		// Invoke trigger if two of the slots are known.
		let n = nounType.nouns.length;

		let ch = ' ';
		let noun = null;
		let i1 = 0, i2 = 0;

		// a) A < B so C is not less than A.
		if (slotA > 0 && slotB > slotA) {
			ch = 'a'; noun = noun3; i1 = 0; i2 = slotA - 1;
		}
		// b) B < A so C is not more than A.
		if (slotB > 0 && slotA > slotB) {
			ch = 'b'; noun = noun3; i1 = slotA; i2 = n;
		}
			// c) A < C so B is not less than A.
		else if (slotA > 0 && slotC > slotA) {
			ch = 'c'; noun = noun2; i1 = 0; i2 = slotA - 1;
		}
			// d) C < A so B is not more than A.
		else if (slotC > 0 && slotA > slotC) {
			ch = 'd'; noun = noun2; i1 = slotA; i2 = n;
		}
			// e) B < C so A is not between B and C.
		else if (slotB > 0 && slotC > slotB) {
			ch = 'e'; noun = noun1; i1 = slotB; i2 = slotC - 1;
		}
			// f) C < B so A is not between C and B.
		else if (slotC > 0 && slotB > slotC) {
			ch = 'f'; noun = noun1; i1 = slotC; i2 = slotB - 1;
		}

		let msg = noun1.name + " is not between " + noun2.name + 
                  " and " + noun3.name + ".";
		for (let i = i1; i < i2; i++) {
			let slot = nounType.nouns[i];
			if (puzzle.getGridVerb(noun, slot) === IsNot) continue;
			//print(noun.name + " is not with " + slot.name);
			rs = solver.addMarkByRule(mark, rule, ch, noun, IsNot, slot, msg);
			if (rs !== 0) return rs;
		}

		return rs;
	}
	return isNotBetween;
};

// ---------------------------------------------------------------------------------
// 7. isRelated

/**
 * Returns the isRelated function to enforce the rule where 
 * noun1 is related to at least one noun in nouns2.
 * See puzzles: AllTiredOut
 * @param {Puzzle} puzzle Puzzle object.
 * @param {Rule} rule Rule.
 * @param {Noun} noun1 Noun 1.
 * @param {Link} link Link.
 * @param {Array} nouns2 Array of nouns.
 * @returns {Function} Function isRelated.
 */
SmartRule.getIsRelated = function (puzzle, rule, noun1, link, nouns2) {
	function isRelated(mark) {
		//print("isRelated rule=" + Q + rule.num + Q + " noun1=" + 
        //Q + noun1 + Q + " link=" + Q + link + Q + " nouns2=" + Q + nouns2 + Q);
		let rs = 0;
		let slots = link.nounType;
		let slot1 = (noun1.type === slots) ? noun1 : Puzzle.getPairNoun(noun1, slots);

		if (slot1 !== null) {
			let nounB, slotB;
			let ok;

			// Violation if all nouns are slotted and noun1 is not related 
            // to any noun in nouns2.
			ok = false;
			for (let noun2 of nouns2) {
				let slot = (noun2.type === slots) ? 
                    noun2 : Puzzle.getPairNoun(noun2, slots);
				if (slot === null || link.f(slot1, slot) === Is) { ok = true; break; }
			}
			if (!ok) return -1;

			// Violation if all slots related to noun1 are full, 
            // and no slot contains a noun in the list.
			// Example: For AllTiredOut, rule 2 is 
            // "Grace stood next to at least one man in line (clue 7)."
			// If Grace is 1st and a woman is 2nd, then this is a violation.
			ok = false;
			for (let slot of slots.nouns) {
				if (link.f(slot1, slot) !== Is) continue;
				for (let noun of nouns2) {
					nounB = Puzzle.getPairNoun(slot, noun.type);
					if (nounB === null || nounB === noun) { ok = true; break; }
				}
				if (ok) break;
			}
			if (!ok) return -1;

			// Violation if all slots related to noun1 cannot have any noun in nouns2.
			ok = false;
			for (let slot of slots.nouns) {
				if (link.f(slot1, slot) !== Is) continue;
				for (let noun of nouns2) {
					if (puzzle.getGridVerb(slot, noun) !== IsNot) { ok = true; break; }
				}
				if (ok) break;
			}
			if (!ok) return -1;

			// Trigger if only one noun in list can be related to noun1, then place it.
			// Example: If I manually place Grace first and Ethan fifth, 
            // then Jeff must be second!
			nounB = null; slotB = null;
			let cnt = 0;
			for (let slot of slots.nouns) {
				if (link.f(slot1, slot) !== Is) continue;
				for (let noun of nouns2) {
					let slotX = Puzzle.getPairNoun(noun, slots);
					if (slotX === slot) {
						//print(noun.name + " is already in " + slot.name);
						cnt = 2;
						break;
					}
					if (slotX !== null) continue;

					if (puzzle.getGridVerb(noun, slot) === Maybe) {
						//print(noun.name + " may be in " + slot.name);
						if (++cnt > 1) break;
						nounB = noun; slotB = slot;
					}
				}
				if (cnt > 1) break;
			}
			//print("cnt=" + cnt);
			if (cnt === 1) {
				let msg = nounB.name + " must be with " + slotB.name + ".";
				//print("Rule " + rule.num + " " + msg);
				rs = solver.addMarkByRule(mark, rule, 'a', nounB, Is, slotB, msg);
				if (rs !== 0) return rs;
			}
		}

		// Trigger if noun1 can be in slotX, 
        // but no noun in list can be related to slotX, then noun1 cannot be in slotX.
		if (slot1 === null) {
			for (let slotX of slots.nouns) {
				if (puzzle.getGridVerb(noun1, slotX) !== Maybe) continue;
				let ok = false;
				let msg = noun1.name + " is not with " + slotX.name + ".";
				for (let slot2 of slots.nouns) {
					if (link.f(slotX, slot2) !== Is) continue;
					for (let noun2 of nouns2) {
						if (puzzle.getGridVerb(noun2, slot2) !== IsNot) {
							ok = true;
							break;
						}
					}
					if (ok) break;
				}
				if (!ok) {
					//print("SmartRule.isRelated Rule " + rule.num + 
                    //" on mark " + mark.num + ". " + msg);
					rs = solver.addMarkByRule
                         (mark, rule, 'b', noun1, IsNot, slotX, msg);
					if (rs !== 0) return rs;
				}
			}
		}

		return rs;
	}
	return isRelated;
};

// --------------------------------------------------------------------------------------
// 8. inOppositeGroup

/**
 * Returns the inOppositeGroup function to enforce the rule 
 * where noun1 and noun2 are not in the same group.
 * See puzzles: Big5GameRangers
 * @param {Puzzle} puzzle Puzzle object.
 * @param {Rule} rule Rule.
 * @param {Noun} noun1 Noun 1.
 * @param {Noun} noun2 Noun 2.
 * @param {NounType} nounType Noun type.
 * @param {Array} map Array of numbers.
 * @param {string} groupName Group name.
 * @param {Array} groupNames Array of group names.
 * @returns {Function} Function inOppositeGroup.
 */
SmartRule.getInOppositeGroup = 
    function (puzzle, rule, noun1, noun2, nounType, map, groupName, groupNames) {
	function inOppositeGroup(mark) {
		let rs = 0;

		let nounA = (noun1.type === nounType) ? 
                     noun1 : Puzzle.getPairNoun(noun1, nounType);
		let nounB = (noun2.type === nounType) ? 
                     noun2 : Puzzle.getPairNoun(noun2, nounType);
		if (nounA === null && nounB === null) return rs;

		let g1 = (nounA === null) ? -1 : map[nounA.num - 1];
		let g2 = (nounB === null) ? -1 : map[nounB.num - 1];

		// Violation if both nouns are in the same group, 
        // otherwise success if both are in opposite groups.
		if (nounA !== null && nounB !== null) {
			return (g1 === g2) ? -1 : 0;
		}

		// Triggers.
		let msg = noun1.name + " and " + noun2.name + 
                  " have the opposite " + groupName + ".";
		for (let noun of nounType.nouns) {
			// If noun1's group is known, then noun2 is not with a noun of that group.
			if (nounA !== null && map[noun.num - 1] === g1) {
				//print(msg);
				rs = solver.addMarkByRule(mark, rule, 'a', noun2, IsNot, noun, msg);
				if (rs !== 0) return rs;
			}
			// If noun2's group is known, then noun1 is not with a noun of that group.
			if (nounB !== null && map[noun.num - 1] === g2) {
				//print(msg);
				rs = solver.addMarkByRule(mark, rule, 'b', noun1, IsNot, noun, msg);
				if (rs !== 0) return rs;
			}
		}

		return rs;
	}
	return inOppositeGroup;
};

// -------------------------------------------------------------------------------------
// 9. inSameGroup

/**
 * Returns the inSameGroup function to enforce the rule where noun1 and noun2 
 * are in the same group.
 * See puzzles: Big5GameRangers
 * @param {Puzzle} puzzle Puzzle object.
 * @param {Rule} rule Rule.
 * @param {Noun} noun1 Noun 1.
 * @param {Noun} noun2 Noun 2.
 * @param {NounType} nounType Noun Type.
 * @param {Array} map Array of numbers.
 * @param {string} groupName Group name.
 * @param {Array} groupNames Array of group names.
 * @returns {Function} Function inSameGroup.
 */
SmartRule.getInSameGroup = 
    function (puzzle, rule, noun1, noun2, nounType, map, groupName, groupNames) {

	// If there are not at least two nouns in each list, then
	// (a) The nouns in list1 are not with noun1, and
	// (b) The nouns in list2 are not with noun2.
	function doListEliminator2(mark, rule, noun1, noun2, list1, list2, msg) {
		let rs = 0;
		for (let noun of list1) {
			rs = solver.addMarkByRule(mark, rule, 'a', noun1, IsNot, noun, msg);
			if (rs !== 0) return rs;
		}
		for (let noun of list2) {
			rs = solver.addMarkByRule(mark, rule, 'b', noun2, IsNot, noun, msg);
			if (rs !== 0) return rs;
		}
		return rs;
	}

	function inSameGroup(mark) {
		let rs = 0;

		let nounA = (noun1.type === nounType) ? 
                     noun1 : Puzzle.getPairNoun(noun1, nounType);
		let nounB = (noun2.type === nounType) ? 
                     noun2 : Puzzle.getPairNoun(noun2, nounType);

		let g1 = (nounA === null) ? -1 : map[nounA.num - 1];
		let g2 = (nounB === null) ? -1 : map[nounB.num - 1];

		// Violation if both nouns are in opposite groups, 
        // otherwise success if both in same group.
		if (nounA !== null && nounB !== null) {
			return (g1 !== g2) ? -1 : 0;
		}

		// Triggers.
		let msg = noun1.name + " and " + noun2.name + 
                  " have the same " + groupName + ".";
		// If noun1's group is known, then noun2 is not with a noun of another group.
		if (nounA !== null && nounB === null) {
			for (let noun of nounType.nouns) {
				if (map[noun.num - 1] === g1) continue;
				//print(msg);
				rs = solver.addMarkByRule(mark, rule, 'a', noun2, IsNot, noun, msg);
				if (rs !== 0) return rs;
			}
		}

		// If noun2's group is known, then noun1 is not with a noun of another group.
		if (nounA === null && nounB !== null) {
			for (let noun of nounType.nouns) {
				if (map[noun.num - 1] === g2) continue;
				//print(msg);
				rs = solver.addMarkByRule(mark, rule, 'b', noun1, IsNot, noun, msg);
				if (rs !== 0) return rs;
			}
		}

		// Examine counts if there are only two groups.
		if (nounA !== null || nounB !== null) return rs;

		// Example is from Big5GameRangers.
		// * Elephant camp can be run by Ethan or Julia.
		// * Buffalo camp can be run by Delia, Ethan, or Julia.
		// If there are less than two nouns in a group, 
        // then those nouns are not possible candidates.

		let group1 = []; let group1Noun1 = []; let group1Noun2 = [];
		let group2 = []; let group2Noun1 = []; let group2Noun2 = [];

		// Populate the lists.
		for (let noun of nounType.nouns) {
			let i = noun.num - 1;
			let verb1 = puzzle.getGridVerb(noun, noun1);
			if (verb1 === Maybe) {
				if (map[i] === 0) group1Noun1.push(noun); else group2Noun1.push(noun);
			}
			let verb2 = puzzle.getGridVerb(noun, noun2);
			if (verb2 === Maybe) {
				if (map[i] === 0) group1Noun2.push(noun); else group2Noun2.push(noun);
			}
			if (verb1 === Maybe || verb2 === Maybe) {
				if (map[i] === 0) group1.push(noun); else group2.push(noun);
			}
		}

		//print(mark.num + " Group 1: " + group1.length + ","  + 
        //group1Noun1.length + "," + group1Noun2.length + " Group 2: " + 
        //group2.length + "," + group2Noun1.length + "," + group2Noun2.length);

		if ((group1.length < 2 || group1Noun1.length < 1 || 
             group1Noun2.length < 1) && group1.length > 0) {
			msg = "There are not enough " + groupNames[0] + 
                  " for " + noun1.name + " and " + noun2.name + ".";
			rs = doListEliminator2
                 (mark, rule, noun1, noun2, group1Noun1, group1Noun2, msg);
			if (rs !== 0) return rs;
		}

		if ((group2.length < 2 || group2Noun1.length < 1 || 
             group2Noun2.length < 1) && group2.length > 0) {
			msg = "There are not enough " + groupNames[1] + 
                  " for " + noun1.name + " and " + noun2.name + ".";
			rs = doListEliminator2
                 (mark, rule, noun1, noun2, group2Noun1, group2Noun2, msg);
			if (rs !== 0) return rs;
		}
		return rs;
	}
	return inSameGroup;
};

全局变量

在本文中,我一直在为我使用全局变量而道歉,但有些全局变量是不可避免的。这里有一些例子。

  • viewer 是 Viewer 对象。在 head.php 中定义,并在谜题的 onload 事件中实例化。
  • solver 是 Solver 对象。在 head.phpWebWorker.js 中定义,但仅由 Web Worker 实例化。
  • puzzlePuzzle 对象的子(谜题模块)。在 head.phpWebWorker.js 中定义和实例化。
  • 谜题模块类。例如:FiveHouses。由 head.phpWebWorker.js 加载。
  • 通用类。由 head.phpWebWorker.js 加载。

注意:文件 head.phpWebWorker.js 将在未来的文章中讨论。

结论

希望您喜欢阅读本文。我撰写本文的动机是希望您能尝试自己建模一个逻辑谜题。然后,我们可以一起找到更好的方法来建模和/或解决逻辑谜题。谢谢!

历史

  • 2017年7月13日:初始版本
© . All rights reserved.