在 JavaScript 中建模一个逻辑谜题
如何在 JavaScript 中建模逻辑网格谜题
目录

引言
本文解释了如何在 JavaScript 编程语言中为逻辑网格谜题建模。逻辑网格谜题,也称为逻辑谜题或逻辑问题,可以在 Dell Magazines 和 Penny Press 的杂志中找到。Mystery Master 网站 http://www.mysterymaster.com 致力于编写可以解决逻辑谜题的软件。可以把它想象成 "天网",但没有消灭人类的意图。本文将重点介绍逻辑谜题 "五座房子"。这个两星谜题的早期版本被称为 "爱因斯坦的谜语" 或 "斑马谜题"。这个版本的谜题出现在思维缜密的 Marilyn vos Savant 的专栏中。在继续阅读之前,请先查看这个逻辑谜题。您应该具备 JavaScript 的基本知识,了解一些新的构造,并理解 JavaScript 中的类如何工作。
JavaScript
JavaScript 是网络的客户端语言。我不认为它是最好的语言,但多年来它已经有所改进。我希望它是强类型的,有更好的作用域规则(public、private 等),但我想我可以使用 TypeScript。它不能做我在 C# 中可以做的一些好事情,比如在实例化对象时初始化它。它没有 空合并运算符 ??。它也非常脆弱——您的 IDE 和/或浏览器可能无法发现您的错误。但抱怨够了……以下是我喜欢的一些东西。
- "use strict";选项。
- 使用 const关键字定义常量。
- 使用 let关键字避免变量提升。
- addEventListener方法,以便事件调用的方法可以是局部的。
- 返回闭包的能力——引用局部变量的函数。有关闭包的更多信息,请参阅 智能链接 和 智能规则 部分。
- 在本地存储中存储/检索信息的能力。
什么是逻辑谜题
逻辑谜题是一个神秘的故事。但你不仅仅是它的读者,你还是它最重要的角色——侦探!就像任何值得拥有望远镜的侦探一样,你必须解开谜团。大多数逻辑谜题都有其属性,例如标题、作者和星级。一星表示谜题容易,五星表示谜题非常困难。每个谜题还配有一幅幽默的图片。虽然这些属性很重要,但它们不会帮助你解决逻辑谜题。真正有帮助的是谜题的线索。线索通常以编号列表的形式给出,但有时也可以在介绍中找到。属性、介绍和线索列表是逻辑谜题的文本。
注意:如果你想自己解决一个逻辑谜题,你可以使用以下工具:图表和网格。这两种形式将在未来的文章中讨论。
要为逻辑谜题建模,必须将逻辑谜题的文本解析为特定类型的数据。解决逻辑谜题所需的一切都必须由这些数据捕获。在继续进行时,有一件重要的事情要记住
逻辑谜题中名词之间的所有关系都必须表示为事实或规则。
请在阅读谜题文本时记住以下问题。
我们需要数据结构来存储每种类型的数据,当我们在面向对象编程的世界中谈论数据结构时,我们谈论的是类。那么,我们应该将存储逻辑谜题所有数据的类命名为什么呢?当然是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();
以下是每行代码的描述
- 将 FiveHouses类设置为继承自基类Puzzle。不幸的是,子类FiveHouses的构造函数现在被设置为父类Puzzle的构造函数。
- 将 FiveHouses的构造函数设置为其自身的构造函数。
- 实例化(创建)一个 FiveHouses类的对象,该对象继承自Puzzle,并调用其自身的构造函数。
我目前不使用 ECMAScript 2015 中引入的 class 和 constructor 关键字。所以当我说构造函数时,我真正的意思是 FiveHouses 函数,我将其视为一个类。让我们讨论一下您在谜题模块中可能看到的每个部分。
属性
属性是逻辑谜题的元数据。我们唯一关心的属性是谜题的名称和标题。这些信息通过 Puzzle 成员 myName 和 myTitle 设置。我使用这些名称而不是更通用的 name 和 title,以避免命名冲突。
 // 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");
添加动词
对于每个谜题模块,总是定义三个名词。这三个全局变量是:IsNot、Maybe 和 Is。由于这些动词的预定义属性是可以接受的,因此我们的谜题模块不需要此部分。
注意:我知道我应该避免使用全局变量,但我也想避免在每个类成员前加上 "this"。所以这是我的折衷方案。
链接
谜题中两个名词之间的所有关系都必须用动词和链接来表达。当你检查谜题中的线索,如果关系不明显,这意味着链接是“with”。例如,我们示例谜题中的第一条线索写道:“英国人住在红房子里。”这条线索可以表达为一个事实,其中第一个名词是“英国人”,动词是肯定的,链接是“with”。任何时候线索表明一个名词与另一个名词在一起或不在一起,只需使用默认链接“with”。换句话说,“住在”这个短语实际上意味着“with”。这个事实的数据可以解释为:fact(英国人, is, with, 红色)。
对于我们的示例谜题,有三个链接:“with”、“right of”和“next to”。所有链接都使用名词类型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: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 的编号。
- X 告诉您事实是否已启用(选中)或已禁用(未选中)。
- 命中数是事实被引用的次数。
- 名称是事实的文本。
事实的类型
事实的类型如下所示。虽然第一种类型的事实只产生一个标记,但其他类型的事实通常产生多个标记。
类型 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.");
 
注意:如您在此事实中看到的,变量 Is 和 With 不需要以 this 关键字作为前缀,因为它们是全局变量。我希望“this”不是一个问题。
这种方法看起来非常简单。但我必须告诉您,这种方法具有重载,您一次可以输入多个事实。因此,让我们在另一篇文章中讨论这些重载是什么。
验证事实
在神秘大师解决逻辑谜题之前,它首先通过寻找各种逻辑错误来验证逻辑谜题。验证谜题的 Puzzle 方法被恰当地命名为 validate。以下是事实无效的一些原因。
- 事实的动词是可能动词(“maybe”)。它必须是肯定动词(“is”)或否定动词(“is not”)。
- 事实中的两个名词相同。事实中的名词必须不同。
- 事实的链接是“with”,但两个名词类型相同。这就像说“Bob不是Abe”,或者“Abe是Bob”。对于逻辑网格谜题,相同类型的两个名词无论如何都不会在一起。
- 事实的链接不是“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 的编号。
- X 告诉您规则是启用(选中)还是禁用(未选中)。
- 命中数是规则被引用的次数。
- 名称是规则的文本。
规则类
/* 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]);
规则和链接一样,都需要编程。每个规则的函数 f 由 SmartRule 静态类中的方法提供。有关更多信息,请参阅 智能规则 部分。规则通常根据标记执行操作,并返回状态码。状态码为负数表示违规,为零表示成功。规则可以执行以下一个或多个任务。
强制违规
当标记与谜题线索矛盾时,就会发生违规。与线索矛盾的标记称为矛盾。这只应在进行假设时发生。违规意味着程序应撤销上次假设,并进行另一次假设。如果未进行假设,则这是一个致命的逻辑错误,程序将停止解决谜题。在谜题“筋疲力尽”中,如果一个标记创建了格蕾丝第一、一个女人第二的情况,则此标记将与线索矛盾。当规则遇到这种情况时,它必须通知程序违规。
触发标记
一个检查当前标记以输入额外标记的规则称为触发器。触发器的状态是提交标记的状态。如果标记没有问题,状态为零。如果标记有问题,状态为负数,并立即返回。如果触发器输入的标记被拒绝,则与规则违规相同。如果触发器成功,则当前规则可以继续,并且可以调用其他规则。
对于逻辑谜题“筋疲力尽”,规则必须寻找这样的情况:“如果没有男人可以排第二,那么格蕾丝就不能排第一。”当发现这种情况时,规则将为“第一”和格蕾丝输入“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.php 和 WebWorker.js 中定义,但仅由 Web Worker 实例化。
- puzzle是 Puzzle 对象的子(谜题模块)。在 head.php 和 WebWorker.js 中定义和实例化。
- 谜题模块类。例如:FiveHouses。由 head.php 和 WebWorker.js 加载。
- 通用类。由 head.php 和 WebWorker.js 加载。
注意:文件 head.php 和 WebWorker.js 将在未来的文章中讨论。
结论
希望您喜欢阅读本文。我撰写本文的动机是希望您能尝试自己建模一个逻辑谜题。然后,我们可以一起找到更好的方法来建模和/或解决逻辑谜题。谢谢!
历史
- 2017年7月13日:初始版本



