· tips  · 8 min read

Breaking Down Self-Executing Puzzles: A Creative Coding Challenge

Learn how to design and implement self-executing puzzles in JavaScript - code constructs that run their solving logic as soon as they are defined. This article gives step-by-step guidance, multiple implementation patterns, worked examples and safety tips.

Learn how to design and implement self-executing puzzles in JavaScript - code constructs that run their solving logic as soon as they are defined. This article gives step-by-step guidance, multiple implementation patterns, worked examples and safety tips.

What you’ll build and why it matters

Imagine writing a puzzle whose code not only describes the challenge but also solves itself the moment you define it. Short feedback loops. Instant gratification. Deep learning.

By the end of this article you’ll be able to design small, safe self-executing puzzles in JavaScript using several idiomatic techniques (IIFEs, classes with lifecycle behavior, proxies and coercion hooks). You’ll also learn how to choose which pattern fits your goals and how to avoid common pitfalls like accidental global effects or security holes.

What is a “self-executing puzzle”?

A self-executing puzzle is a piece of code that contains both the definition of a challenge and the logic to solve or validate it, and that runs that logic automatically at definition time (or on the first meaningful access). In practice this means the code produces output or registers a result the moment it is loaded, without an extra explicit call like solve().

Why do this?

  • Quick feedback for learning and testing.
  • Interesting domain-specific DSLs (domain-specific languages) where puzzle description and solver live together.
  • Fun programming exercises: you design puzzles that evaluate themselves.

Patterns you’ll use (quick tour)

  • IIFE (Immediately Invoked Function Expression) - run as soon as defined.
  • Classes or constructors that run logic in their initialization.
  • Getters and Proxy - run solver when properties are accessed or operations are performed.
  • Coercion hooks such as Symbol.toPrimitive - run on conversion.

Each pattern has trade-offs. IIFE is simple and explicit. Proxy and coercion let you make puzzles that feel inert until used.

Pattern 1 - The simple IIFE puzzle

Outcome: a tiny puzzle that runs as soon as the JS parser evaluates it.

Step-by-step:

  1. Write a function that contains both the puzzle data and the solver.
  2. Immediately invoke it so it runs on definition.

Example - “find two numbers that sum to target”:

(function () {
  const numbers = [2, 7, 11, 15];
  const target = 9;

  function findPair(nums, t) {
    const seen = new Map();
    for (let i = 0; i < nums.length; i++) {
      const needed = t - nums[i];
      if (seen.has(needed)) return [seen.get(needed), i];
      seen.set(nums[i], i);
    }
  }

  const result = findPair(numbers, target);
  console.log('IIFE puzzle result:', result); // [0, 1]
})();

Why this is useful: extremely explicit. Good for demos, tests and embedded puzzles in a file. But it runs immediately and cannot be paused or registered unless you add a registry.

Pattern 2 - Puzzle class that registers itself

Outcome: define many puzzles that auto-register and optionally auto-run. Good when you want a puzzle suite.

Step-by-step:

  1. Create a small registry that holds puzzles.
  2. Let puzzle constructors push themselves to the registry and optionally run.

Example:

const PuzzleRegistry = [];

class Puzzle {
  constructor(name, data, autoRun = true) {
    this.name = name;
    this.data = data;
    PuzzleRegistry.push(this);
    if (autoRun) this.solve();
  }

  solve() {
    // Default: override in subclasses or instances
    console.log(`${this.name} has no solver.`);
  }
}

// Define a self-executing instance
new Puzzle('sum-to-target', { nums: [3, 4, 1], target: 7 }, true);

// You can query all puzzles
console.log(
  'Registered puzzles:',
  PuzzleRegistry.map(p => p.name)
);

Extend this pattern with subclassing so each puzzle type includes specialized solver logic. This approach makes it easy to collect metadata, track runs and test results.

Pattern 3 - Lazy-executing puzzles via Proxy or getters

Outcome: puzzle appears inert but runs only when you interact with it (e.g., read a property). Useful to avoid side-effects until the developer explicitly inspects or uses the puzzle.

Here’s a Proxy example that runs the solver when you access .answer:

function makeLazyPuzzle(spec) {
  let solved = false;
  let result;

  const target = {
    spec,
  };

  return new Proxy(target, {
    get(obj, prop) {
      if (prop === 'answer') {
        if (!solved) {
          // run solver only once
          result = simpleSubsetSolver(obj.spec.nums, obj.spec.target);
          solved = true;
        }
        return result;
      }
      return Reflect.get(obj, prop);
    },
  });
}

function simpleSubsetSolver(nums, target) {
  // small backtracking demo: find subset that sums to target
  const res = [];
  function backtrack(i, sum, path) {
    if (sum === target) return path.slice();
    if (sum > target || i === nums.length) return null;
    path.push(nums[i]);
    const a = backtrack(i + 1, sum + nums[i], path);
    if (a) return a;
    path.pop();
    return backtrack(i + 1, sum, path);
  }
  return backtrack(0, 0, []) || null;
}

const lazy = makeLazyPuzzle({ nums: [5, 3, 2], target: 8 });
console.log('Puzzle defined. No solver run yet.');
console.log('Answer:', lazy.answer); // solver runs here

This provides control over when the solver executes while still keeping the self-executing feel when you inspect the puzzle.

For more on Proxy objects see the MDN guide: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Pattern 4 - Clever coercion: Symbol.toPrimitive and self-run on conversion

Outcome: run solver when the puzzle is coerced to a primitive - for example when concatenated into a string or used in arithmetic.

This can be used to make puzzles that solve when someone logs them or concatenates them to form a message.

Example:

function makeCoerciblePuzzle(spec) {
  let solved = false;
  let result;

  return {
    spec,
    [Symbol.toPrimitive](hint) {
      if (!solved) {
        result = simpleSubsetSolver(this.spec.nums, this.spec.target);
        solved = true;
      }
      // return a string for logging
      return `CoerciblePuzzle(${JSON.stringify(result)})`;
    },
  };
}

const p = makeCoerciblePuzzle({ nums: [1, 2, 3, 4], target: 6 });
console.log('Will coerce when printed: ' + p); // solver runs during coercion

See MDN on Symbol.toPrimitive for details: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive

A worked, slightly larger example - Self-executing Mini-Riddle Registry

Goal: create a lightweight framework where each riddle is defined with a short DSL and solves itself, recording success and time. Then provide a small riddle example.

Key steps:

  1. Build a registry to collect riddles and results.
  2. Provide a defineRiddle helper that accepts the riddle text, inputs and a solver function.
  3. When defineRiddle runs, it executes the solver and stores result/time.

Implementation:

const RiddleBook = [];

function defineRiddle({ id, text, input }, solverFn) {
  const start = performance.now();
  let solved = false;
  let output, error;
  try {
    output = solverFn(input);
    solved = true;
  } catch (e) {
    error = String(e);
  }
  const time = performance.now() - start;
  const entry = { id, text, input, solved, output, error, time };
  RiddleBook.push(entry);
  // optionally log a concise result
  console.log(
    `Riddle ${id}: ${solved ? 'solved' : 'failed'} in ${time.toFixed(2)}ms`
  );
  return entry;
}

// Define a riddle: "find a permutation of digits 1..4 that forms 1234 when concatenated"
// trivial example but shows DSL
defineRiddle(
  {
    id: 'perm-1',
    text: 'Find a permutation of digits that sums to 10',
    input: { nums: [1, 2, 3, 4] },
  },
  ({ nums }) => {
    // small solver: find subset that sums to 10
    function backtrack(i, sum, path) {
      if (sum === 10) return path.slice();
      if (sum > 10 || i === nums.length) return null;
      path.push(nums[i]);
      const a = backtrack(i + 1, sum + nums[i], path);
      if (a) return a;
      path.pop();
      return backtrack(i + 1, sum, path);
    }
    return backtrack(0, 0, []) || 'no solution';
  }
);

console.log(RiddleBook);

This pattern is production-friendly: no proxies, no coercion tricks. Instead it explicitly runs when defined and records everything for later analysis.

Debugging and testing self-executing puzzles

  • Avoid noisy global side-effects. Prefer registries or logging to a dev console instead of mutating global variables.
  • Time your solver (performance.now()) when you want to benchmark.
  • If using lazy patterns, ensure the solver runs at most once (memoize the result) to avoid inconsistent states.
  • Use small input sizes while developing; move to optimized solutions later.

Security and safety notes

  • Never feed untrusted input into eval or dynamic Function constructors. They can execute arbitrary code.
  • Keep side-effects (file writes, network calls) explicit and well-controlled. Self-executing code that silently performs network calls can be surprising and dangerous.
  • For puzzles that run in the browser, be aware of long-running computations freezing the UI - use setTimeout, requestIdleCallback, or Web Workers for heavy solvers.

MDN’s security notes on eval: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval

Ideas to extend and gamify

  • Make a web page that lists self-executing puzzles; when a user hovers or clicks a puzzle, it reveals the solution (use lazy proxies to avoid precomputing everything).
  • Create a “puzzle authoring DSL” where authors write concise puzzle definitions and the platform executes them to validate submissions.
  • Turn the registry into a leaderboard: auto-run tests and record performance, then sort puzzles by solving time.
  • Use Web Workers to run puzzle solvers off the main thread and post results back when done.

Common pitfalls and how to avoid them

  • Pitfall: solver runs multiple times unexpectedly (e.g., property accessed repeatedly). Solution: memoize results and set a solved flag.
  • Pitfall: accidental global pollution. Solution: use closures, modules or registries.
  • Pitfall: long-running computation blocks UI. Solution: offload to Worker or chunk computation via setTimeout.

Final notes - what this teaches

Building self-executing puzzles forces you to think about API shape, lifecycle and side-effects. You learn to balance immediacy (run now) with control (run later). You learn patterns for registration, lazy evaluation and graceful error reporting.

And the best part: these puzzles make your code teachable. Instead of calling solve() in a REPL over and over, you wire behavior into definition itself and watch the program reveal its reasoning. That feedback loop accelerates learning in a way that few exercises can.

When you build puzzles that execute themselves, you aren’t just writing code - you’re designing experiences that teach, test and entertain, all in one go.

References

Back to Blog

Related Posts

View All Posts »
Array Manipulation in a Single Line

Array Manipulation in a Single Line

Learn to transform arrays with concise, expressive one-liners using map, filter, reduce and related tools. Practical patterns: sum, unique elements, flattening, grouping, counting, and safe chaining.