JSFiddle - React, Tailwind, and code Playground

HTML

<textarea id=codes></textarea><br>
<button onclick="update()">Update</button>
<button onclick="step()">Step</button>
<p id=states>

CSS

p{font-family:monospace}

JavaScript

var rules = [], state = "";
function update(){
  let code = codes.value.split(/\n::=\n(.*)/s);
  rules = code[0].split`\n`.filter(a=>a);
  state = code[1]||'';
  states.innerText = state;
  states.style.color = "black";
}
function step(){
  let matchingRules = [];
  for (let rule of rules){
    let allMatches = matches(rule);
    if (allMatches.length){
      matchingRules.push([rule,allMatches]);
    }
  }
  if (!matchingRules.length){
    states.style.color = "#202020";
    return;
  }
  let ruleMatch = chooseRandom(matchingRules);
  let rule = ruleMatch[0];
  let match = chooseRandom(ruleMatch[1]);
  let toReplace = rule.split('::=').slice(1).join('::=');
  toReplace = toReplace.replace(/\\n/g,'\n');
  [...toReplace.matchAll(/\$(\d+)/g)].reverse().forEach(a=>{
  	toReplace = toReplace.substring(0,a.index)+(match[0][Number(a[0].slice(1))]||"")+toReplace.substring(a.index+a[0].length,toReplace.length);
  });
  console.log(match, toReplace);
  if (toReplace[0]=="~"){
    alert(toReplace.substring(1))
    toReplace="";
  }
  toReplace = toReplace.replace(/:::/g,prompt)
  state = state.substring(0,match[1]) + toReplace + state.substring(match[2],state.length);
  states.innerText = state;
}
function chooseRandom(items){
  return items[Math.floor(Math.random()*items.length)];
}
function matches(rule){
  let re = RegExp(rule.split('::=')[0],'gs');
  let matches = [];
  while ((match = re.exec(state)) != null){
    matches.push([match, match.index, re.lastIndex]);
    if (match.index===re.lastIndex) re.lastIndex++;
  }
  return matches;
}