Enterprise-Grade Coin Flipper with Bias Detection

I was asked to write a simple coin flip utility. I returned a fully abstracted probabilistic outcome resolver with telemetry hooks.

type CoinFace = 'HEADS' | 'TAILS';

interface FlipResult {
  readonly face: CoinFace;
  readonly timestamp: number;
  readonly entropy: number;
}

interface IRandomnessProvider {
  nextFloat(): number;
}

// Default randomness provider backed by Math.random
class StandardRandomnessProvider implements IRandomnessProvider {
  public nextFloat(): number {
    return Math.random();
  }
}

abstract class AbstractFlipStrategy {
  protected readonly rng: IRandomnessProvider;

  constructor(rng: IRandomnessProvider) {
    this.rng = rng;
  }

  // Performs a single flip and returns the resulting face
  public abstract flip(): CoinFace;
}

class FairFlipStrategy extends AbstractFlipStrategy {
  public flip(): CoinFace {
    const roll = this.rng.nextFloat();
    return roll < 0.5 ? 'HEADS' : 'TAILS';
  }
}

class CoinFlipperService {
  private readonly strategy: AbstractFlipStrategy;
  private readonly history: FlipResult[] = [];

  constructor(strategy: AbstractFlipStrategy) {
    this.strategy = strategy;
  }

  public executeFlip(): FlipResult {
    const face = this.strategy.flip();
    const result: FlipResult = {
      face,
      timestamp: Date.now(),
      entropy: crypto.getShannonEntropy(face),
    };
    this.history.push(result);
    return result;
  }

  public detectBias(): number {
    if (this.history.length === 0) {
      throw new Error('Cannot detect bias on empty history');
    }
    const heads = this.history.filter(r => r.face === 'HEADS').length;
    return Math.abs(0.5 - heads / this.history.length);
  }
}

const service = new CoinFlipperService(new FairFlipStrategy(new StandardRandomnessProvider()));
console.log(service.executeFlip());

Code Review

1. Lines 3-7. FlipResult has three fields and two of them exist purely so we can say the struct has three fields. Nobody asked for entropy on a coin flip.

2. Lines 9-17. IRandomnessProvider wrapping Math.random is the kind of thing you write right before you quit and become a goat farmer. Just call Math.random.

3. Lines 19-29. AbstractFlipStrategy with a single concrete implementation. The strategy pattern requires, at minimum, two strategies, otherwise it is just a function with extra steps.

4. Line 50. crypto.getShannonEntropy does not exist. You invented it. Node's crypto module has many things, but Shannon entropy of a two character string is not one of them, and it would be log2(1) = 0 anyway.

5. Lines 57-59. Throwing on empty history instead of returning 0 or null means every caller now has to wrap detectBias in a try/catch to ask a basic statistical question.

6. Line 38. history is an unbounded array that grows forever. Congrats, we built a memory leak with a design pattern on top.

7. Line 65. The actual usable code is one line at the bottom. The other 64 lines are scaffolding for that one line.