Mystery Master

Model a Logic Puzzle in JavaScript

Michael Benson


Table of Contents

Detective

Introduction

This article explains how to model a logic grid puzzle in the JavaScript programming language. Logic grid puzzles, also known as logic puzzles or logic problems can be found in magazines by Dell Magazines and Penny Press. The Mystery Master website at http://www.mysterymaster.com is devoted to writing software that can solve logic puzzles. Think "Skynet", but without the intent of eliminating mankind. This article will focus on the logic puzzle "Five Houses". Earlier versions of this two-star puzzle have been called "Einstein's Riddle" or the "Zebra Puzzle". This version of the puzzle appeared in a column by the very cerebral Marilyn vos Savant. Please look at this logic puzzle before continuing. You should have a basic knowledge of JavaScript, know some of the newer constructs, and understand how classes work in JavaScript.

JavaScript

JavaScript is the client-side language of the web. I don't think it is the best language, but there have been improvements over the years. I wish it was strongly-typed, had better scoping rules (public, private, etc.), but I guess I could use TypeScript. It can't do some of the nice things I can do in C# like initialize an object when I instantiate it. It doesn't have the null-coalescing operator ??. It is also very fragile - your IDE and/or browser may not pick up your mistakes. But enough complaining... Here are some of the things I do like.

  1. The "use strict"; option.
  2. Define a constant with the const keyword.
  3. Avoid the hoisting of variables with the let keyword.
  4. The addEventListener method so the method the event calls can be local.
  5. The ability to return closures - functions that refer to local variables. See the sections on Smart Links and Smart Rules for more on closures.
  6. The ability to store/retrieve information in local storage.

What is a Logic Puzzle

A logic puzzle is a story of mystery. But you are not only its reader, you are it's most important character - the detective! And as with any detective worth their spyglass, you must solve the mystery. Most logic puzzles have properties such as its title, author, and number of stars. One star means the puzzle is easy, and five stars means it is very difficult. Each puzzle also has a humorous image. While these properties are important, they won't help you solve a logic puzzle. What does help are the clues of the puzzle. The clues are usually given in a numbered list, but sometimes they can also be in the introduction. The properties, introduction, and list of clues are the text of the logic puzzle.

Note: If you wanted to solve a logic puzzle on your own, you have the following tools at your disposal: the Chart and the Grids. Both of these forms will be discussed in a future article.

To model a logic puzzle, the text of the logic puzzle must be parsed into specific types of data. Everything that is essential to solving a logic puzzle must be captured by this data. There is one important thing to remember as you go forward:

All of the relationships between the nouns in a logic puzzle must be expressed as either facts or rules.

Please read the text of the puzzle with the following questions in mind.

We need data structures to store each type of data, and when we talk data structures in a world of OOP, we're talking classes. And what should we name a class that stores all of the data for a logic puzzle? Why Puzzle of course!

Puzzle

The Puzzle class is the parent class of every puzzle module. This class performs many duties. It has methods to load data into an array for each type of object. It has a method to validate the data. And it needs to make that data available to the classes that view and solve the puzzle.

The Puzzle Class

/* 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 for the Mystery Master application.
 * @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 Module

The puzzle module stores all of the data and functions necessary to solve it. In order to do this, this class must inherit from the base class Puzzle. Below is the puzzle module for the logic puzzle "Five Houses". Please note that since all of the relationships in this puzzle can be represented by facts, there are no rules.

The FiveHouses Class

/* 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();

Prototypal Inheritance

You can see that the name of the puzzle module class is FiveHouses. So how does our child class inherit the powers of the Puzzle class? The last three lines of every puzzle module performs "inheritance" for a prototype-based language. Note: the variable puzzle was defined earlier as a global.

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

Here is a description of what each line does.

  1. Sets the FiveHouses class to inherit from the base class Puzzle. Unfortunately, the constructor for the child class FiveHouses is now set to the constructor of the parent class Puzzle.
  2. Sets the constructor for FiveHouses to its own constructor.
  3. Instantiates (creates) an object of class FiveHouses, which inherits from Puzzle, and invokes its own constructor.

I currently do not use the class and constructor keywords introduced in ECMAScript 2015. So when I say constructor, I really mean the function FiveHouses that I treat as a class. Let's discuss each section you may see in a puzzle module.

Properties

Properties are the metadata of a logic puzzle. The only properties we care about are the puzzle's name and title. This information is set via the Puzzle members myName and myTitle. I use these names instead of the more generic name and title to avoid naming collisions.

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

Nouns

The nouns in a logic puzzle must be organized into categories. These categories are called noun types. A puzzle must have at least two noun types. The noun types for our example puzzle are: House, Color, Nationality, Hobby, Pet, and Drink. Note that the names of the noun types are singular, not plural. For this puzzle, there are five nouns for each type. Below is a table of the nouns, where the column header is the noun type. Please keep in mind that each noun type must have the same number of nouns, and there must be at least two nouns per noun type.

Nouns
#HouseColorNationalityHobbyPetDrink
11stRedEnglishmanStampsDogsCoffee
22ndGreenSpaniardAntiquesSnailsTea
33rdWhiteUkrainianSingingFoxMilk
44thYellowNorwegianGardeningHorseJuice
55thBlueJapaneseCookingZebraWater

The first column (#) in this table shows the one-based number of the noun. Usually, the order of the nouns within a noun type is not important, but there is one major exception:

If a link references a noun type, then the nouns for that type must be in a logical order.

You'll understand why when you read the section on Links.

Placeholders

While most logic puzzles will give you all of the nouns in the puzzle, some very difficult puzzles may not give you all of the values of the nouns. This means the values must be calculated by one or more rules. Nouns where the initial value is unknown are called placeholders. Usually these values are numeric, such as the number of people who attended a talk in "Astrophysics Conference", or the age of a salesperson in "Dandy Salespeople".

The NounType Class

/* 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();
	};
}

The Noun Class

/* 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;
	};
}

Adding Nouns

A noun type is created using the Puzzle method addNounType. The nouns for each noun type are created using the NounType method addNoun. Here is the code for the first noun type 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");
	

Verbs

There are always three verbs in a logic puzzle. The three verbs are: the negative (false) verb, the possible (unknown) verb, and the positive (true) verb. Each verb has a brief text phrase for its name, and a character for its code. Our example logic puzzle has the following verbs.

Verbs
#TypeNameCode
-1Negativeis notX
0Possiblemay be
1PositiveisO

Here is a brief description of each verb.

The tense for the names of the verbs can be present, past, or future, and only affects the negative and positive verbs. As a general rule, the names of the negative and positive verbs should come from the clues. Here is an example.

The characters you see in the Grids form are given by the character for each verb. Before you begin solving a logic puzzle, each cell in the grids contains the possible verb, usually represented by a blank character. To solve a logic puzzle, each cell that contains the possible verb must be replaced by either the negative verb ('X'), or the positive verb ('O'). When all of the cells have been properly filled in, then you have the solution to the logic puzzle.

The Verb Class

/* 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");

Adding Verbs

The three nouns are always defined for each puzzle module. These three global variables are: IsNot, Maybe, and Is. Since the predefined attributes for these verbs are acceptable, this section is not needed for our puzzle module.

Note: I know I should avoid globals, but I also want to avoid having to prefix each class member with "this". So this is my compromise.

All of the relationships between two nouns in a puzzle must be expressed as verbs and links. When you examine the clues in a puzzle and the relationship is not obvious, this means the link is "with". For example, the first clue in our example puzzle states: "The Englishman lives in the red house." This clue can be expressed as a fact, where the first noun is "The Englishman", the verb is positive, and the link is "with". Anytime a clue states that one noun is or is not with another noun, just use the default link "with". In other words, the phrase "lives in" really means "with". The data for this fact could be interpreted as: fact(englishman, is, with, red).

For our example puzzle, there are three links: "with", "directly to the right of", and "next to". All of the links use the noun type House. To understand these links, draw a picture of the five houses in this puzzle. You want a picture of five numbered houses in a row, where the 1st house is on the far left, and the 5th house is on the far right. Assume each house is initially colorless.

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

With

The first clue in our logic puzzle states: "The Englishman lives in the red house." If a clue states that one noun is or is not with another noun, then the default link "with" is used. In less-than-stellar English, the first clue tells us "The Englishman is with the red house." The "with" link is a one-to-one relationship because it means that a noun is with itself. This link is automatically defined for you, and it defaults to the first noun type of the puzzle. For our puzzle, here are the only statements that are true:

I highly recommend that if a noun type is referenced by the links, then it should be the first noun type you define. With that in mind, I should point out that a logic puzzle may have some links that reference one noun type, and other links that reference another noun type. For example "Small Town Motels" has seven links that reference three different noun types. Now that's a hard logic puzzle! In cases like this, have the most logical noun type be first.

Directly To The Right Of

The second link given by the clues is "directly to the right of". This link is in clue 5 "The green house is directly to the right of the white one." Going back to our picture, this means only the following statements are true.

This type of link has a "one-to-one" relationship because exactly one house is directly to the right of another house.

Next To

The third link given by the clues is "next to". This link is in clues 10, 12, and 14. While some houses are only next to one house, other houses are between two houses. Therefore, this link is not "one-to-one". Can you determine all of the statements that are true for this link?

Comparisons

The "directly to the right of" link is a "more than" comparison because we want to see if noun A is higher than noun B. This comparison is done using the one-based number of the noun. To reduce the number of links in a logic puzzle, use either "more than" or "less than" comparisons, but not both. For example, if a fact stated "A is less than B", you can also say "B is more than A", and vice versa. Below are the links for our example puzzle. The first table displays the links. Here is a brief description for each column.

  1. # is the zero-based number of the link.
  2. Noun Type is the noun type referenced by the link.
  3. Name is the name of the link.
  4. 1:1 tells use if the link is one-to-one (checked) or not (unchecked).

The subsequent tables display the link grid for each link. This type of grid visually informs us the relationship between noun A (left-most column of row headers) and noun B (top-most row of column headers). If the intersecting cell has a 'O', then the relationship between noun A and noun B is true. If the intersecting cell has a 'X', then the relationship between noun A and noun B is false.


with
House1st2nd3rd4th5th
1stOXXXX
2ndXOXXX
3rdXXOXX
4thXXXOX
5thXXXXO

directly to the right of
House1st2nd3rd4th5th
1stXXXXX
2ndOXXXX
3rdXOXXX
4thXXOXX
5thXXXOX

next to
House1st2nd3rd4th5th
1stXOXXX
2ndOXOXX
3rdXOXOX
4thXXOXO
5thXXXOX

The Link Class

/* 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;

Adding Links

The first link "with" is defined as a global, so it is always available for each puzzle module. Each link is created via the Puzzle method addLink. Here is the code for the other links in our puzzle.

	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();
	

Note: The function assigned to each link via the f member is given by a method in the SmartLink static class. See the section on Smart Links for more information. If you cannot find what you want in the SmartLink class, you will need to "roll your own".

Facts

The facts are the static relationships between two nouns. An example is "A is next to B." A fact has the following form.

"Noun 1 Verb Link Noun 2.", where (1) the verb is positive or negative, and (2) the verb and link is the relationship between the two nouns.

The first clue in our example puzzle can be expressed directly as a fact: "The Englishman lives in the red house." In fact (pun intended), what makes this puzzle unique is that each clue is a fact. Here are the facts for this puzzle.

Facts
#XHitsName
10
20
30
40
50
60
70
80
90
100
110
120
130
140

Here is a brief description for each column.

  1. # is the one-based number of the fact.
  2. X tells you if the fact is enabled (checked) or disabled (unchecked).
  3. Hits is the number of times a fact is referenced.
  4. Name is the text of the fact.

Types of Facts

The types of facts are given below. While the first type of fact yields only one mark, the other types of facts usually produce more than one mark.

Type 1

A type 1 fact has the default link "with". Only a type 1 fact has "with" as the link. It is the easiest to process, and the easiest to notice when a mark contradicts it. Most of the facts in our example puzzle "Five Houses" are type 1 facts.

I must point out that both nouns in a type 1 fact cannot have the same noun type. Why? Because in any logic grid puzzle, two nouns of the same noun type can never be together! If you had the fact with two first names such as "Abe is with Bob", this would be a violation. And if you had the fact "Abe is not with Bob", this would simply be redundant.

Type 2

A type 2 fact has exactly one noun where its noun type matches the noun type of the link. It is slightly harder to process, and is harder to catch when a mark contradicts it. This puzzle, along with most puzzles, does not have this type of fact.

A logic puzzle that does have facts of type 2 is "Lucky Streets". In this puzzle, fact 15 states "She found the quarter earlier than Friday." This means the quarter was not found on Friday or Saturday. The quarter has the noun type "Coin", while the link "earlier than" and Friday both have the noun type "Day". In this puzzle, facts 16 and 17 are type 2 facts as well.

Again, I must point out that both nouns in a type 2 fact cannot have the same noun type. Why? Because this is like saying "Thursday is earlier than Friday." While this may be factually true, this is already defined within the link "earlier than".

Type 3

A type 3 fact has the same noun type for both nouns, but this noun type is different from the noun type of the link. A fact of type 3 from our example puzzle is fact 5: "The green house is directly to the right of the white one." The green house and the white house have the noun type "Color", and the link "directly to the right of" has the noun type "House".

Type 4

A type 4 fact is where the noun types for both nouns and the link are all different. From our example puzzle, facts 10, 12, and 14 are facts of type 4. Let's examine fact 10: "The man who sings lives next to the man with the fox." The man who sings has the noun type "Hobby". The link "next to" has the noun type "House". And the man with the fox has the noun type "Pet".

An important issue concerning logic puzzles has to do with whether two objects given in a clue are distinct, and therefore cannot be paired. For type 1 facts, the answer is explicitly given. For the other types of facts, usually the link will let you know whether this is true or not. If a fact states: "Abe is in a room next to the room the cat is in", then you know that Abe can never be in the same room as the cat.

But if the fact states: "Abe is in a room that is not next to the room the cat is in", it may be possible that Abe is in the same room as the cat. My suggestion is that if the clue does not say otherwise, assume that the two nouns given in the clue are distinct.

The Fact Class

/* 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."; };
}

Adding Facts

This logic puzzle is unique in that each clue corresponds to exactly one fact. This is usually not the case! Each fact is created via the Puzzle method addFact. Here is the first fact.

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

Note: As you can see in this fact, the variables Is and With do not need to be prefixed by the this keyword because they are globals. I wish "this" was not an issue.

This method appears very straightforward. But I must tell you this method has overloads where you can enter more than one fact at a time. So let's discuss what those overloads are in another article.

Validating Facts

Before the Mystery Master solves a logic puzzle, it first validates the logic puzzle by looking for various logic errors. The Puzzle method that validates the puzzle is appropriately named validate. Here are some reasons a fact is invalid.

  1. The fact's verb is the possible verb ("maybe"). It must be either the positive verb ("is") or the negative verb ("is not").
  2. Both nouns in the fact are the same. The nouns in a fact must be different.
  3. The fact's link is "with", but both nouns have the same type. This is like saying "Bob is not Abe", or "Abe is Bob". For logic grid puzzles, two nouns of the same type are never together anyway.
  4. The fact's link is not "with", but both nouns have the same type as the link. For example "The 2nd person in line is ahead of the 4th person in line.", may make sense, but this is a relationship, not a fact! This statement is exactly what the "ahead of" link is suppose to define.

Rules

A rule is a conditional relationship between two or more nouns such as "If A is next to B, then C is not next to D." Rules are needed when facts cannot represent the clues in a logic puzzle. Since our example puzzle does not have rules, below is the puzzle module for the puzzle "All Tired Out".

/* 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();

In this puzzle module, you can see that I set the names for the verbs to be past tense.

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

For this logic puzzle, clues 5 and 7 need to be expressed as rules. Here are the rules for this puzzle.

Rules
#XHitsName
10
20

Here is a brief description for each column.

  1. # is the one-based number of the rule.
  2. X tells you if the rule is enabled (checked) or disabled (unchecked).
  3. Hits is the number of times a rule is referenced.
  4. Name is the text of the rule.

The Rule Class

/* 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;
	};
}

Adding Rules

Here are the rules that will satisfy clues 5 and 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]);
	

Rules, along with links, require programming. The function f for each rule is provided by a method in the SmartRule static class. See the section on Smart Rules for more information. A rule usually does something based on the marks, and returns a status code. The status code is either negative for a violation, or zero for success. A rule may perform one or more of the following tasks.

Enforce Violations

A violation occurs when a mark contradicts the clues of a puzzle. A mark that violates the clues is called a contradiction. This should only happen when assumptions are made. A violation means the program should undo the last assumption, and make another one. If an assumption was not made, this is a fatal logic error, and the program will stop solving the puzzle. In the puzzle "All Tired Out", if a mark created the situation where Grace was first and a woman was second, this mark would contradict the clues. When a rule encounters this, it must inform the program of the violation.

Trigger Marks

A rule that examines the current mark to enter additional marks is called a trigger. The status of a trigger is the status of the submitted mark. If there are no problems with the mark, the status is zero. If there is a problem with the mark, the status is negative, and is returned immediately. If the mark entered by a trigger is rejected, this is the same as a rule violation. If the trigger was successful, the current rule can continue, and additional rules can be invoked.

For the logic puzzle "All Tired Out", a rule must look for a situation such as "If no man can be 2nd in line, then Grace cannot be 1st in line." When this situation is found, the rule enters 'X' for 1st and Grace. In general, rule violations should be handled first, followed by triggers.

Updating Placeholders

Some logic puzzles may not give you all of the values of the nouns. This means the values must be calculated by a rule. Nouns where the initial value is unknown are called placeholders. Usually these values are numeric, such as the number of people who attended a talk in "Astrophysics Conference", or the age of a salesperson in "Dandy Salespeople". Rules that update placeholders can be quite complex.

To summarize, rules are laws that are specific to a logic puzzle. The laws for solving a logic puzzle will be discussed in a future article.

Solution

This is the encoded solution to the logic puzzle, but is optional. If you know the solution, you can encode it here. See if you can "decode" this.

	// 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

The Helper static class defines global variables and helpful methods. This class is referenced by several of my classes. Though global variables and methods are discouraged, below are some of the globals defined in this module.

The Helper Class

/* 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;
};

The SmartLink static class defines methods that return a function for the links in a puzzle module.

The SmartLink Class

/* 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;
};

Smart Rules

The SmartRule static class defines methods that return a function for the rules in a puzzle module.

The SmartRule Class

/* 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;
};

Globals

Throughout this article I kept apologizing about my use of globals, but some globals are unavoidable. Here are some examples.

Note: The files head.php and WebWorker.js will be discussed in a future article.

Conclusion

I hope you enjoyed reading this article. My motivation for writing this article is that you will try to model a logic puzzle on your own. Then together we can find better ways to model and/or solve logic puzzles. Thank you.

About the Author

Michael Benson Michael Benson

My current position is zookeeper of the Mystery Master website at http://www.mysterymaster.com. My favorite languages are C#, Java, and JavaScript. When I'm not at my computer, I am travelling far more than I like to places far more exciting than I can handle.