Teaching Puzzle Solving Without Ruining the Puzzle: An Idle Help Teaser System


Kids ages 2-7 get stuck on puzzles. That’s fine — frustration is part of learning. But there’s a threshold where stuck becomes distressed, and a distressed kid closes the app.

The standard solution is a hint button. We didn’t want a hint button. A button requires the kid to recognize they need help and ask for it — that’s a metacognitive step that 3-year-olds don’t reliably do. We wanted help that arrives before the crying starts.

The result: an idle teaser system that watches for inactivity, gently animates the correct tile toward its target, then puts it back. No auto-solving. The kid still has to do the work — they just got a nudge in the right direction.

The Design Constraint

The hardest constraint: the hint should be helpful without being a giveaway.

If you pick up the tile and slide it all the way to the target, you’ve solved the puzzle step for them. That’s not help — that’s removing the challenge. We wanted the teaser to show which tile to move and roughly where, without completing the move.

The animation we landed on:

  1. Lift the tile slightly (scale up + shadow)
  2. Drift it 30% of the way toward its correct position
  3. Pause briefly
  4. Spring back to original position

The kid sees the tile float up and lean toward its home. The direction is unmistakable. But they still have to drag it there themselves.

Idle Detection

The teaser triggers after 8 seconds of no tile interaction. “Interaction” means any drag start — not taps, not scrolls, not anything else. We only care about whether the player is actively attempting to solve.

// client/src/games/puzzle/components/PuzzleBoard.tsx
const HELP_DELAY_BASE = 8000;

function getHelpDelay(helpScore: number): number {
  // As player improves, wait longer before offering help
  // helpScore 0 → 8s, helpScore 10 → 20s, helpScore 20+ → never
  if (helpScore >= 20) return Infinity;
  return HELP_DELAY_BASE + helpScore * 600;
}

helpScore starts at 0 for each puzzle session and increments every time the player successfully places a tile. As they demonstrate competence, the help delay grows. At helpScore >= 20, the teaser disables entirely — this player has it under control.

This means a struggling player gets help after 8 seconds. A player who’s been successfully placing tiles gets increasingly longer delays, and eventually no hints at all. The system fades away as it becomes unnecessary.

The Animation

The teaser is a pure CSS transform animation driven by GSAP. No tile state changes — the tile doesn’t actually move in the puzzle data, only visually:

async function playTeaser(tile: TileRef, targetOffset: { x: number; y: number }) {
  const el = tile.current;
  if (!el) return;

  // Lift
  await gsap.to(el, {
    scale: 1.08,
    boxShadow: '0 12px 32px rgba(0,0,0,0.35)',
    duration: 0.2,
    ease: 'power2.out',
  });

  // Drift toward target (30% of the way)
  await gsap.to(el, {
    x: targetOffset.x * 0.3,
    y: targetOffset.y * 0.3,
    duration: 0.5,
    ease: 'power1.inOut',
  });

  // Pause
  await sleep(400);

  // Check if player grabbed it during the pause
  if (playerInteracted) return;

  // Spring back
  await gsap.to(el, {
    x: 0, y: 0,
    scale: 1,
    boxShadow: 'none',
    duration: 0.35,
    ease: 'back.out(1.7)',
  });
}

The spring-back uses back.out easing — it overshoots slightly and settles, giving it a physical feel. Kids find it delightful rather than intrusive.

Interruption Handling

The tricky part is what happens when the player starts dragging during the animation. There are three states to handle:

Player grabs the hinted tile mid-drift: The teaser should immediately hand off control. We clear the GSAP animation, reset outlineOffset (used for visual lift), and let the drag handler take over from whatever position the tile is at.

Player grabs a different tile: The teaser should abort and return the hinted tile to its rest position. We call playTeaser cancellation and start the idle timer on the new interaction.

Player places a tile correctly: Increment helpScore, reset the idle timer with the new (longer) delay.

function handleTileInteraction() {
  // Cancel any running teaser
  cancelTeaser();

  // Increment score if this was a successful placement
  if (wasCorrectPlacement) {
    setHelpScore(prev => prev + 1);
  }

  // Reset idle timer with current help delay
  resetIdleTimer(getHelpDelay(helpScore));
}

Getting the interruption paths right took several iterations. The failure mode is a tile that ends up at a non-zero transform offset because the spring-back was interrupted without resetting the CSS — it looks like a bug but it’s just a missing outlineOffset reset in one of the cancel paths.

Choosing Which Tile to Hint

We don’t hint randomly. We pick the tile that’s been unmoved the longest and is furthest from its target position — the one most likely to be the source of confusion.

function selectTileToHint(tiles: Tile[]): Tile {
  return tiles
    .filter(t => !t.isPlaced)
    .sort((a, b) => {
      const distA = distance(a.position, a.correctPosition);
      const distB = distance(b.position, b.correctPosition);
      // Weight by distance and time since last moved
      return (distB + b.idleMs / 1000) - (distA + a.idleMs / 1000);
    })[0];
}

This heuristic works well in practice. The tile that looks most “lost” gets hinted first. On a 3x3 grid this is usually obvious to the adult watching, but not necessarily to a 3-year-old.

The helpScore Reset

One subtlety: helpScore resets to 0 on puzzle initialization, not on completion. When a kid starts a new puzzle, they start fresh — no accumulated “you’ve proven yourself” credit from the previous one. A 4x4 puzzle after a successful 3x3 is still a new challenge.

We had a bug early on where helpScore persisted across puzzles, meaning a player who completed several easy puzzles would never see hints on a hard one. The fix was a single setHelpScore(0) in the puzzle init effect.

What We Didn’t Build

A few things we explicitly left out:

Auto-solve button. This would undermine the whole point. If you can just tap “solve it for me,” the puzzle is no longer a puzzle. The teaser is help, not a shortcut.

Hint counter / limit. We considered limiting hints per puzzle. Decided against it — a struggling 3-year-old doesn’t need artificial friction, they need support. The helpScore degradation handles the “fading away” naturally.

Verbal hints. “Try moving that one!” as a voice line. Too intrusive. The visual nudge communicates the same thing more quietly.

The teaser system has been running in production for a few weeks now. The signal we’re watching: session length for our youngest users (the 2-3 age group). Early data suggests fewer rage-quits on the harder grid sizes. That’s the whole goal.