<div class="simon">
  <kbd data-freq="185"></kbd>
  <kbd data-freq="247"></kbd>
  <kbd data-freq="310"></kbd>
  <kbd data-freq="369"></kbd>
  
  <div class="masthead">
    <div>
      <h1>simon</h1>
      <div>
        <samp>00</samp>
        <b></b>
      </div>
    </div>
  </div>
</d>

$green: #009856;
$red: #d91e26;
$yellow:#ffe900;
$blue: #0f7cc5;
$black:#1b1f20;
$green2: #10a652;

@function size($val) {
  @return calc(var(--size) / 200 * #{$val} * 16)
}

*, ::before, ::after {
  box-sizing:border-box;
}

:root{
  --green: #{$green};
  --red: #{$red};
  --yellow: #{$yellow};
  --blue: #{$blue};
  --black: #{$black};
  --green2: #{$green2};
  --size: 40vmin;
}

html, body {height:100%;}

@media (prefers-color-scheme: dark) {
  body {background:#292929;}
}

body {
  margin:0;
  display:flex;
}
body >* {margin:auto;}

body {perspective: size(40);}
body >* {
  transform: rotateX(35deg) translateY(size(-2.5));
  box-shadow: 0 size(.5) 0em size(1) var(--black);
  border-radius: 100%;
}

.simon {
  display:inline-grid;
  grid-template-columns: repeat(2, var(--size));
  gap:size(1);
  background-color:var(--black);
  
  position:relative;
  
  transition:opacity .15s;
  &.disabled {opacity:.85}
  
  .masthead {
    font-size:size(1);
    display:flex;
    align-items:center;
    justify-content:center;
    position:absolute;
    inset:calc(50% - 20%);
    background:var(--black);
    color:white;
    border-radius:100%;
    font-family:sans-serif;
    text-align:center;
  }
  
  .masthead h1+div {
    display:inline-flex; align-items:center; gap:size(.5);
    
    samp {
      border: size(.05) solid rgba(255,255,255,.5);
      padding: size(.15) size(.3);
      border-radius:size(.1);
    }
    
    b {
      width:size(1.5);height:size(1.5);
      background-color:var(--green2);
      box-shadow:0 size(-.075) 0 0 darken($green2, 10%) inset;
      border-radius:100%;
      border:0;
      cursor:pointer;
      
      transition:box-shadow .1s;
      &:active {
        box-shadow:0 size(.075) 0 0 var(--black) inset;
      }
    }
  }
  
  >kbd {
    height:var(--size);
    cursor:pointer; user-select:none;

    transition:box-shadow .1s;
    overflow:hidden;
    
    &:nth-child(1) {
      background-color:var(--green); border-top-left-radius:var(--size);
      background-position:right bottom;
      box-shadow:0 size(-.5) 0 0 darken($green, 10%) inset;

      &.active, &:active {
        background-image: radial-gradient(circle at right bottom, white, transparent 100%);
        box-shadow:0 size(.5) 0 0 var(--black) inset;
      }
    }
    &:nth-child(2) {
      background-color:var(--red); border-top-right-radius:var(--size);
      box-shadow:0 size(-.5) 0 0 darken($red, 10%) inset;

      &.active, &:active {
        background-image: radial-gradient(circle at left bottom, white, transparent 100%);
        box-shadow:0 size(.5) 0 0 var(--black) inset;
      }
    }
    &:nth-child(3) {
      background-color:var(--yellow); border-bottom-left-radius:var(--size);
      box-shadow:0 size(-.5) 0 0 darken($yellow, 10%) inset;

      &.active, &:active {
        background-image: radial-gradient(circle at right top, white, transparent 100%);
        box-shadow:0 size(.5) 0 0 var(--black) inset;
      }
    }
    &:nth-child(4) {
      background-color:var(--blue); border-bottom-right-radius:var(--size);
      box-shadow:0 size(-.5) 0 0 darken($blue, 10%) inset;

      &.active, &:active {
        background-image: radial-gradient(circle at left top, white, transparent 100%);
        box-shadow:0 size(.5) 0 0 var(--black) inset;
      }
    }
  }
}

View Compiled
class Button {
  constructor($el) {
    this.$el = $el;
    this.freq = $el.dataset.freq;
  }
  async play() {
    this.$el.classList.add("active");
    await playNote(this.freq);
    this.$el.classList.remove("active");
  }
}

class Simon {
  constructor($el) {
    this.$el = $el
    const $buttons = [...$el.querySelectorAll("kbd")]

    this.buttons = $buttons.map(($el) => new Button($el))
    
    this.$score = $el.querySelector('samp')

    //
    $buttons.forEach(($el, i) => {
      $el.onclick = (e) => {
        if (this.replaying) return
        
        const button = this.buttons[i];
        button.play();
        this.user.push(button)
        
        this.check()
      };
    });

    this.sequence = []
    this.user = []
    
    this.starting = undefined
    this.replaying = undefined
  }

  check() {
    if (!this.started) return
    
    let okSofar = true
    for (const [i, button] of this.user.entries()) {
       if (button !== this.sequence[i]) {
         okSofar = false
         break;
       }
    }
    // console.log('okSofar', okSofar)
    
    const reset = () => {
      this.user = [];
      this.replay(500);
    }
    
    if (!okSofar) {
      console.log("nop");

      reset()
      return 
    }
    
    if (this.user.length >= this.sequence.length) {
      console.log("win");
      this.score++
      this.updateScore()

      this.add();
      reset()
    }
  }
  
  updateScore() {
    this.$score.innerText = `${this.score}`.padStart(2, '0')
  }

  getRandomButton() {
    return this.buttons[Math.floor(Math.random() * this.buttons.length)];
  }

  add() {
    this.sequence.push(this.getRandomButton());
  }

  async replay(pauseintro = 0) {
    this.replaying = true
    this.$el.classList.add('disabled')
    
    await wait(pauseintro);
    for (const [i, button] of this.sequence.entries()) {
      await button.play();
      if (i < this.sequence.length - 1) await wait(lerp(Simon.INTERVAL_MIN, Simon.INTERVAL_MAX, this.score/Simon.INTERVAL_MAX_SCORE));
    }
    
    this.$el.classList.remove('disabled')
    this.replaying = false
  }
  
  async start() {
    if (this.starting) return
    this.starting = true
    this.started = false
    
    this.score = 0
    this.updateScore()
    
    this.user = []
    this.sequence = []
    
    await this.startMusic()
    await wait(1000)
    
    this.add()
    this.add()
    await this.replay()
    
    this.starting = false
    this.started = true
  }
  
  async startMusic() {
    const buttons = new Array(4).fill(1).map((el, i) => this.buttons.at(i))
    for (const [i, button] of buttons.entries()) {
      await button.play();
    }
  }
}
Simon.INTERVAL_MIN = 500
Simon.INTERVAL_MAX = 20
Simon.INTERVAL_MAX_SCORE = 15

const simon = new Simon(document.querySelector(".simon"));
async function start() {
  simon.start()
}
document.querySelector("b").onclick = start;

function startOnce() {
  simon.start()
  document.querySelector(".simon").removeEventListener('click', startOnce)
}
document.querySelector(".simon").addEventListener('click', startOnce)























// create web audio api context
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();

function wait(t) {
  return new Promise((resolve) => setTimeout(resolve, t));
}

function lerp (start, end, amt){
  return (1-amt)*start+amt*end
}

async function playNote(frequency = 440, duration = 100) {
  // create Oscillator node
  var oscillator = audioCtx.createOscillator();

  oscillator.type = "square";
  oscillator.frequency.value = frequency; // value in hertz
  oscillator.connect(audioCtx.destination);
  oscillator.start();

  await wait(duration);
  oscillator.stop();
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.