A perceptual randomizer that makes random sequences feel more random by preventing recent repetitions.
True randomness can feel clustered and repetitive. This utility generates cryptographically random numbers while maintaining a buffer of recent values, ensuring a more natural distribution that humans perceive as "truly random."
Pure random number generators can produce jarring repetitions—the same card twice in a row, back-to-back sound effects, or repeated quiz questions. While mathematically random, these patterns feel wrong to users. UniqueRandomGenerator solves this by tracking recently generated values and avoiding them, creating sequences that are both random and perceptually smooth.
- 🎲 Cryptographically secure random generation using Web Crypto API
- 🔄 Configurable history buffer to prevent recent repetitions
- ⛓️ Chainable API for clean, readable code
- 🎯 Perfect for games, quizzes, playlists, and UI variety
- 🪶 Zero dependencies, TypeScript-native
import { UniqueRandomGenerator } from '@paperscissors/unique-random-generator';
const gen = new UniqueRandomGenerator()
  .setMinMax(1, 10)
  .setBufferSize(3); // Remember last 3 values
const number = gen.getRandomIntegerInRange(); // Never repeats the last 3 numbersnpm install @paperscissors/unique-random-generatorOr with yarn:
yarn add @paperscissors/unique-random-generatorOr with pnpm:
pnpm add @paperscissors/unique-random-generatorimport { UniqueRandomGenerator } from '@paperscissors/unique-random-generator';
// Create a generator for dice rolls that doesn't repeat the last 2 rolls
const dice = new UniqueRandomGenerator()
  .setMinMax(1, 6)
  .setBufferSize(2);
console.log(dice.getRandomIntegerInRange()); // e.g., 4
console.log(dice.getRandomIntegerInRange()); // e.g., 2 (not 4)
console.log(dice.getRandomIntegerInRange()); // e.g., 6 (not 2)function createShuffledDeck(numCards: number = 52) {
  const generator = new UniqueRandomGenerator()
    .setMinMax(0, numCards - 1)
    .setBufferSize(numCards); // Remember all cards for complete shuffle
  return Array.from({ length: numCards }, () =>
    generator.getRandomIntegerInRange()
  );
}
const deck = createShuffledDeck();
console.log(deck); // All cards, shuffled, no repeatsclass SoundEffectRandomizer {
  private generator: UniqueRandomGenerator;
  private sounds: string[];
  constructor(sounds: string[]) {
    this.sounds = sounds;
    this.generator = new UniqueRandomGenerator()
      .setMinMax(0, sounds.length - 1)
      .setBufferSize(Math.min(3, sounds.length - 1)); // Avoid last 3 sounds
  }
  play() {
    const index = this.generator.getRandomIntegerInRange();
    const sound = this.sounds[index];
    console.log(`Playing: ${sound}`);
    return sound;
  }
}
const footsteps = new SoundEffectRandomizer([
  'step1.mp3', 'step2.mp3', 'step3.mp3', 'step4.mp3'
]);
// Natural-sounding footsteps without jarring repeats
for (let i = 0; i < 10; i++) {
  footsteps.play();
}interface Question {
  id: number;
  text: string;
  difficulty: 'easy' | 'medium' | 'hard';
}
class QuizSelector {
  private generator: UniqueRandomGenerator;
  private questions: Question[];
  constructor(questions: Question[]) {
    this.questions = questions;
    this.generator = new UniqueRandomGenerator()
      .setMinMax(0, questions.length - 1)
      .setBufferSize(Math.floor(questions.length * 0.75)); // Remember 75% of questions
  }
  getNextQuestion(): Question {
    const index = this.generator.getRandomIntegerInRange();
    return this.questions[index];
  }
}new UniqueRandomGenerator()Creates a new instance with default settings.
Sets the range (inclusive) for random number generation.
generator.setMinMax(1, 100); // Generate numbers from 1 to 100Sets only the minimum value.
generator.setMin(0);Sets only the maximum value.
generator.setMax(99);Sets how many recent values to remember and avoid.
generator.setBufferSize(5); // Avoid last 5 generated numbersGenerates a random integer that hasn't been recently generated.
// Use configured range
const num = generator.getRandomIntegerInRange();
// Override range for this call only
const num2 = generator.getRandomIntegerInRange(1, 10);- Cryptographic Randomness: Uses Web Crypto API (crypto.getRandomValues()) for secure random generation, compatible with Node.js 18+ and all modern browsers
- Buffer Tracking: Maintains an internal array of recently generated values
- Smart Avoidance: Regenerates numbers that exist in the buffer until finding a fresh value
- Dynamic Adaptation: Buffer size can be auto-adjusted based on range when not explicitly set
# Install dependencies
pnpm install
# Run tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Run tests with coverage
pnpm test:coverage
# Build the package
pnpm build
# Build in watch mode
pnpm dev
# Lint code
pnpm lint
# Format code
pnpm format
# Type check
pnpm typecheckunique-random-generator/
├── src/
│   ├── index.ts                    # Main entry point with exports
│   └── UniqueRandomGenerator.ts    # Core class implementation
├── tests/
│   └── UniqueRandomGenerator.test.ts # Test suite
├── dist/                           # Build output (generated)
├── tsconfig.json                   # TypeScript configuration
├── tsup.config.ts                  # Build configuration
├── vitest.config.ts                # Test configuration
└── package.json                    # Package metadata
Before publishing to npm:
# This runs automatically via prepublishOnly
pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build
# Publish to npm (requires authentication)
npm publish --access public- Strict avoidance: Set buffer size to 50-75% of your range
- Natural feeling: Set buffer size to 20-30% of your range
- Complete shuffle: Set buffer size equal to your range
- Minimal repetition: Set buffer size to 1-3 for small ranges
✅ Good for:
- UI element selection (backgrounds, colors, animations)
- Game mechanics (levels, enemies, power-ups)
- Audio (music playlists, sound effects)
- Content delivery (quiz questions, flashcards)
- Any scenario where perceived randomness matters
❌ Not ideal for:
- Cryptographic applications (use pure crypto.getRandomValues)
- Statistical sampling (use unbiased random)
- Security-critical randomness (predictability is introduced by the buffer)
- Efficient for typical ranges (hundreds to thousands of values)
- Buffer checking is O(n) where n is buffer size
- For very large ranges with large buffers, consider pre-generating values
- For extreme non-repetition needs, consider Fisher-Yates shuffle instead
When used correctly, this increases the perception of randomness by aligning random generation with human expectations of variety. True randomness includes clusters and repetitions that feel "broken" to users. By preventing recent repetitions, we create sequences that are both mathematically random and experientially satisfying.
MIT
Contributions welcome! Please open an issue or PR.
Made with ❤️ for better user experiences