Last active
March 27, 2026 16:47
-
-
Save jimdelois/c37409101c9da9d4dfadecfdbaec60c2 to your computer and use it in GitHub Desktop.
Example Logicstuffs for reusability within Venn.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * THE POINT of this file is to illustrate how you could create reusable | |
| * (and eventually even composable) Criteria while also completely avoiding | |
| * ever having to pre-compile answer sets. | |
| * | |
| * SENDING the entire answer sets up to the browser for the game results to | |
| * be calculated on the client-side is NO BUENO. It undermines the whole shebang! | |
| * | |
| * NOTE: This file does nothing to resolve the issues with the current "save game results" | |
| * logic, but it DOES provide several building blocks that would be re-used if active game | |
| * state would not be persisted to the DB or to the in-memory cache store. | |
| * | |
| * That is for another Gist altogether. :-) | |
| */ | |
| /** | |
| * A "Base class" to provide structure around what the concept of a Criterion is and represents | |
| * If JavaScript allowed for "Abstract" classes, this would be an abstract class implementing an interface. | |
| * | |
| * NOTE that all subclasses/implementations are discretely and deterministically unit-testable. | |
| * That is wildly important in the real world. | |
| */ | |
| class Criterion { | |
| constructor(label) { | |
| this.label = label; | |
| } | |
| // This method would be the "abstract" one in a more-powerful language | |
| isSatisfiedBy(input) { | |
| throw "This is an abstract base class that must be overridden."; | |
| } | |
| } | |
| /** | |
| * An actual implementation of a Criterion | |
| */ | |
| class EndsWithLetter extends Criterion { | |
| constructor(lastLetter) { | |
| super(`Ends with ${lastLetter}`); | |
| this.lastLetter = lastLetter; | |
| } | |
| isSatisfiedBy(input) { | |
| return input[input.length-1] === this.lastLetter; | |
| } | |
| } | |
| /** | |
| * Another actual implementation of a Criterion | |
| */ | |
| class DoubleLetter extends Criterion { | |
| constructor() { | |
| super("Has double letters"); | |
| } | |
| isSatisfiedBy(input) { | |
| let prevLetter = input[0]; | |
| for (let i=1; i<input.length; i++) { | |
| if (prevLetter === input[i]) { return true; } | |
| prevLetter = input[i]; | |
| } | |
| return false; | |
| } | |
| } | |
| /** | |
| * The following few classes show how a well-considered base class | |
| * can be sublcassed for legibility and expressiveness, but essentially | |
| * still keeps all the logic centralized. | |
| */ | |
| class LengthConstraint extends Criterion { | |
| constructor(lenMin = Number.NEGATIVE_INFINITY, lenMax = Number.POSITIVE_INFINITY) { | |
| let label = `Is between ${lenMin} and ${lenMax} letters long`; | |
| if (lenMin === Number.NEGATIVE_INFINITY) { | |
| label = `Is at most ${lenMax} letters long`; | |
| } else if (lenMax === Number.POSITIVE_INFINITY) { | |
| label = `At least ${lenMin} letters long` | |
| } else if (lenMin === lenMax) { | |
| label = `Is exactly ${lenMin} letters long` | |
| } else if (lenMax < lenMin) { | |
| throw `Invalid LengthConstraint with inputs ${lenMin} and ${lenMax}`; | |
| } | |
| super(label); | |
| this.lenMin = lenMin; | |
| this.lenMax = lenMax; | |
| } | |
| isSatisfiedBy(input) { | |
| return input.length >= this.lenMin && input.length <= this.lenMax; | |
| } | |
| } | |
| class AtLeastLength extends LengthConstraint { | |
| constructor(len) { super(len, Number.POSITIVE_INFINITY); } | |
| } | |
| class AtMostLength extends LengthConstraint { | |
| constructor(len) { super(Number.NEGATIVE_INFINITY, len); } | |
| } | |
| class ExactLength extends LengthConstraint { | |
| constructor(len) { super(len, len); } | |
| } | |
| /** | |
| * Example COMPOSITION of Criteria via "Decoration" | |
| * Could be refactored to take in N criteria | |
| * Could also create a similar one with OR logic instead of AND | |
| */ | |
| class DualConstraint extends Criterion { | |
| constructor(crit1, crit2) { | |
| super(`${crit1.label} and ${crit2.label.toLowerCase()}`); | |
| this.crit1 = crit1; | |
| this.crit2 = crit2; | |
| } | |
| isSatisfiedBy(input) { | |
| return this.crit1.isSatisfiedBy(input) && this.crit2.isSatisfiedBy(input); | |
| } | |
| } | |
| /** | |
| * This is an example of how simple bit operations can generate a unique "Region ID" | |
| * by only testing each criterion one single time. It's not necessary to use or understand | |
| * this function, but it is an example of how it would be done in the real world because | |
| * it scales infinitely and removes "fat fingering" errors trying to map IDs all over the place | |
| */ | |
| function getRegionId(input, criteria = []) { | |
| let mask = 0; | |
| for (let i = 0; i < criteria.length; i++) { | |
| console.log(`Testing input "${input}" against "${criteria[i].label}"`) | |
| // The operation (N << X) results in the value (N * 2^X). This is known as "bit shifting" | |
| // This single line of code converts the index (i) of the criterion object to a region ID or "Region Bit": | |
| // Criterion at idx 0 => Region 1 = 0b001 | |
| // Criterion at idx 1 => Region 2 = 0b010 | |
| // Criterion at idx 2 => Region 4 = 0b100 | |
| const regionBit = (1 << i); | |
| // If the criterion is true for this input, we now add the "Region Bit" | |
| // for that criterion to the running total we're calling "Mask" | |
| // This also illustrates the power of encapsulating logic behind an "interface" | |
| // such as "isSatisfiedBy". We have achieved agnosticism and genericism, having | |
| // separated "what we do with an outcome" from "how do we calculate an outcome." | |
| if (criteria[i].isSatisfiedBy(input) === true ) { | |
| mask += regionBit | |
| } | |
| } | |
| return mask; | |
| } | |
| /** | |
| * This would be the ONLY thing you would ever have to commit to the repository to ensure the | |
| * games keep running, aside from any new Criteria objects. | |
| * You could create the mapping as many days into the future as you'd like. | |
| * | |
| * There is technically a downside to this *exactly as written,* which you can ask me about, but | |
| * resolving that here would convolute the overall point being made. This would work fine for a long time. | |
| */ | |
| const dailyMapping = { | |
| // "2026-03-21": [new HasExactLength(7), new StartsWithLetter("B"), new HasScrabbleScore(HasScrabbleScore.GREATER_THAN, 12)], | |
| "2026-03-25": [new EndsWithLetter("r"), new AtLeastLength(8), new DoubleLetter()], | |
| "2026-03-27": [new EndsWithLetter("t"), new DoubleLetter(), new LengthConstraint(5, 7)], | |
| } | |
| /** | |
| * In reality, you would be serving up puzzles daily, with fallback logic, | |
| * validations to handle overnight/UTC issues, etc. Other state management, etc. | |
| */ | |
| // const puzzleId = (new Date()).toISOString().split('T')[0]; | |
| // But for this simple example, we hardcode it. | |
| const puzzleId = "2026-03-25"; | |
| /** | |
| * The centralized logic on the server side would load the right puzzle | |
| * at the right time, and then ultimately call through to getRegionId. | |
| * | |
| * That call to getRegionId would send the result back to the browser, | |
| * and the browser would now be informed as to where to send the word they | |
| * just entered, without ever having "leaked out" any answers to the user. | |
| */ | |
| const activePuzzleCriteria = dailyMapping[puzzleId] | |
| /** | |
| * EXAMPLES for the list of Criteria in "2026-03-25" | |
| */ | |
| const exampleWordsToTest = [ | |
| // No matches | |
| // Number of possible outcomes is 1 (Combinatorially "3 choose 0") | |
| "test", // ~A & ~B & ~C = 0b000 = 0 | |
| // One in each region | |
| // Number of possible outcomes is 3 (Combinatorially "3 choose 1") | |
| "tester", // A & ~B & ~C = 0b001 = 1 | |
| "something", // ~A & B & ~C = 0b010 = 2 | |
| "rabbit", // ~A & ~B & C = 0b100 = 4 | |
| // Overlapping of two regions only | |
| // Number of possible outcomes is 3 (Combinatorially "3 choose 2") | |
| "reservoir", // A & B & ~C = 0b011 = 3 | |
| "rebuttal", // ~A & B & C = 0b110 = 6 | |
| "butter", // A & ~B & C = 0b101 = 5 | |
| // Overlapping of all three regions | |
| // Number of possible outcomes is 1 (Combinatorially "3 choose 3") | |
| "abattoir", // A & B & C = 0b111 = 7 | |
| ]; | |
| // Log the results for each example | |
| for (const word of exampleWordsToTest) { | |
| const regionId = getRegionId(word, activePuzzleCriteria); | |
| console.log(regionId); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Output of the above script: