在 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日:初始版本