Skip to content

Instantly share code, notes, and snippets.

@jimdelois
Last active March 27, 2026 16:47
Show Gist options
  • Select an option

  • Save jimdelois/c37409101c9da9d4dfadecfdbaec60c2 to your computer and use it in GitHub Desktop.

Select an option

Save jimdelois/c37409101c9da9d4dfadecfdbaec60c2 to your computer and use it in GitHub Desktop.
Example Logicstuffs for reusability within Venn.
/**
* 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);
}
@jimdelois

Copy link
Copy Markdown
Author

Output of the above script:

Testing input "test" against "Ends with r"
Testing input "test" against "At least 8 letters long"
Testing input "test" against "Has double letters"
0
Testing input "tester" against "Ends with r"
Testing input "tester" against "At least 8 letters long"
Testing input "tester" against "Has double letters"
1
Testing input "something" against "Ends with r"
Testing input "something" against "At least 8 letters long"
Testing input "something" against "Has double letters"
2
Testing input "rabbit" against "Ends with r"
Testing input "rabbit" against "At least 8 letters long"
Testing input "rabbit" against "Has double letters"
4
Testing input "reservoir" against "Ends with r"
Testing input "reservoir" against "At least 8 letters long"
Testing input "reservoir" against "Has double letters"
3
Testing input "rebuttal" against "Ends with r"
Testing input "rebuttal" against "At least 8 letters long"
Testing input "rebuttal" against "Has double letters"
6
Testing input "butter" against "Ends with r"
Testing input "butter" against "At least 8 letters long"
Testing input "butter" against "Has double letters"
5
Testing input "abattoir" against "Ends with r"
Testing input "abattoir" against "At least 8 letters long"
Testing input "abattoir" against "Has double letters"
7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment