0

I've read many other posts regarding this common issue, and I encourage anyone who sees this to read this entire post through. None of the other solutions that I've found have worked for me (I include a failed attempt below).

I have a functioning random generator that uses HTML and JavaScript. Each time a button is pushed, the function chooses a single item from an array and displays it using: 'document.getElementById'. Please see the below snippet for the working function. My problem is that I dislike the way it displays the same array items back to back or before some of the others have been see; the function is TOO RANDOM. I've been working on finding a way to change my random function so that it only displays repeat items once the entire array has been looped through.

var oddnumber = [
  '111',
  '222',
  '333',
  '444',
  '555',
]
var oddletter = [
  'AAA',
  'BBB',
  'CCC',
  'DDD',
  'EEE',
]
function newThing() {
  if(numberCheck.checked) {
    var randomY = oddnumber;
    }
  if(letterCheck.checked) {
    var randomY = oddletter;
  }
  var randomX = Math.floor(Math.random() * (randomY.length));
  var y = randomY;
  var x = randomX;
  document.getElementById("thingDisplay").innerHTML = y[x];
}
<body>
  <div id='thingDisplay'></div>
  <div>
    <button class="box" id="button01" onclick="newThing()">New Thing</button>
  </div>
  <div>
    <form>
      Number<label>&nbsp;<input type="radio" name="thing" id="numberCheck"/></label>
      <br/>Letter<label>&nbsp;<input type="radio" name="thing" id="letterCheck"/></label>
    </form>
  </div>
</body>

Many answers detail different ways to slice the array and push the slice to the bottom, but I'm unsure if this is what I'm looking for. Placing displayed items in a separate array is something I'd like to avoid since I will probably have thousands of array items in real world use, so it probably wouldn't be efficient.

Failed Attempt 1:

var oddnumber = [
  '111',
  '222',
  '333',
  '444',
  '555',
]
var oddletter = [
  'AAA',
  'BBB',
  'CCC',
  'DDD',
  'EEE',
]
function newThing() {
  if(numberCheck.checked) {
    var randomY = oddnumber;
    }
  if(letterCheck.checked) {
    var randomY = oddletter;
  }
  
  var res = randomY.sort(function() {
    return 0.5 - Math.random();
  });
  console.log(res.slice(randomY,1))
  document.getElementById("thingDisplay").innerHTML = [console.log];
}
<body>
  <div id='thingDisplay'></div>
  <div>
    <button class="box" id="button01" onclick="newThing()">New Thing</button>
  </div>
  <div>
    <form>
      Number<label>&nbsp;<input type="radio" name="thing" id="numberCheck"/></label>
      <br/>Letter<label>&nbsp;<input type="radio" name="thing" id="letterCheck"/></label>
    </form>
  </div>
</body>

Failed Attempt 2:

var oddnumber = [
  '111',
  '222',
  '333',
  '444',
  '555',
]
var oddletter = [
  'AAA',
  'BBB',
  'CCC',
  'DDD',
  'EEE',
]
function newThing() {
    if(numberCheck.checked) {
        var randomY = oddnumber;
    }
    if(letterCheck.checked) {
        var randomY = oddletter;
    }
    var selected;
    var temp;
    var str = "";
    var stub = "";

    for(var i = 0; i < randomY.length; i++){
        temp = randomY[i][Math.floor(Math.random() * randomY[i].length)];
        while(selected.contains(temp)){
            temp = randomY[i][Math.floor(Math.random() * randomY[i].length)];
        }
    selected.push(temp);
    str += temp;
    if(i < randomY.length - 1){str += stub;}
    }

    var x = i;
    document.getElementById("thingDisplay").innerHTML = y[x];
}
<body>
  <div id='thingDisplay'></div>
  <div>
    <button class="box" id="button01" onclick="newThing()">New Thing</button>
  </div>
  <div>
    <form>
      Number<label>&nbsp;<input type="radio" name="thing" id="numberCheck"/></label>
      <br/>Letter<label>&nbsp;<input type="radio" name="thing" id="letterCheck"/></label>
    </form>
  </div>
</body>

Edit: Preserving the 'document.getElementById' and the 'if' statements that determine the value of randomY is crucial.

guyw
  • 19
  • 7
  • do you have an example of wanted not too random randomness? – Nina Scholz Jul 28 '20 at 19:02
  • @NinaScholz Yes, preventing the function from displaying repeat items (ie: 222 multiple time before any other is displayed) and only displaying the same item once every other item has been shown at least once. – guyw Jul 28 '20 at 19:04
  • Does this answer your question? [How to efficiently randomly select array item without repeats?](https://stackoverflow.com/questions/17891173/how-to-efficiently-randomly-select-array-item-without-repeats) – SMAKSS Jul 28 '20 at 19:05
  • maybe this is one ..., you are looking for: https://stackoverflow.com/questions/40056297/random-number-which-is-not-equal-to-the-previous-number – Nina Scholz Jul 28 '20 at 19:06
  • @SMAKSS I'm afraid that is another answer that didn't work for my situation. – guyw Jul 28 '20 at 19:09
  • @guyw Well, I think you are wrong. All you have to do is to create a copy from your current array, pop the selected item from the copied array, choose from remaining ones, do this until all item get exhausted then do that all over again. That should work fine in your current situation without a doubt. Try [this one](https://stackoverflow.com/a/17891411/11908502) – SMAKSS Jul 28 '20 at 19:12
  • @NinaScholz this one could work, but I'm unsure of how to switch out the 'console log' for 'document.getElementById=innterHTML =' If you think it could work, could you perhaps submit an answer that includes a snippet? – guyw Jul 28 '20 at 19:15
  • @SMAKSS It's very possible I am wrong, as I am a novice. However, if this solution can work with my situation, I haven't succeeded in applying it. Would you consider submiting an answer with a snippet if you understand how to make it work? – guyw Jul 28 '20 at 19:17

3 Answers3

1

The canonical method is to simply shuffle your array using e.g. the Fisher-Yates shuffle algorithm, and then choosing items in order from it, until you've exhausted the list, at which point you shuffle again. (At that point, you could take care to see that the first item in the newly shuffled list wasn't the one you picked last.)

// To make it easier to store the states for the "decks" we can pick items from,
// they're stored in an object, not in free variables:

const values = {
  "number": [
    '111',
    '222',
    '333',
    '444',
    '555',
  ],
  "letter": [
    'AAA',
    'BBB',
    'CCC',
    'DDD',
    'EEE',
  ],
};

// Keeps track of the shuffled pile of values per type.
const shuffledValues = {};

// Keeps track of the last value picked per type.
const lastValue = {};

// via https://bost.ocks.org/mike/shuffle/
function shuffle(array) {
  let m = array.length,
    t, i;
  while (m) {
    i = Math.floor(Math.random() * m--);
    t = array[m];
    array[m] = array[i];
    array[i] = t;
  }
  return array;
}

function populateDebug() {
  document.getElementById("debug").value = `
shuffledValues = ${JSON.stringify(shuffledValues)}
lastValue = ${JSON.stringify(lastValue)}
`.trim();
}

function newThing() {
  const checkedTypeRadio = document.querySelector("input[name=thing]:checked");
  const type = checkedTypeRadio ? checkedTypeRadio.value : null;
  if (!values[type]) return; // No type chosen, or it is an invalid one.
  if (!shuffledValues[type] || !shuffledValues[type].length) {
    // No shuffled values left? Shuffle a new one,
    // and take care that the new first value is
    // not the last value we picked.
    do {
      shuffledValues[type] = [...shuffle(values[type])];
    } while (shuffledValues[type][0] === lastValue[type]);
  }
  // Pick off the next value from the shuffled pile.
  const nextValue = shuffledValues[type].shift();
  // Save it for the take-care check.
  lastValue[type] = nextValue;
  // Print it out.
  document.getElementById("thingDisplay").innerHTML = nextValue;

  populateDebug();
}
<div id='thingDisplay'></div>
<button class="box" id="button01" onclick="newThing()">New Thing</button>
<div>
  <label>Number&nbsp;<input type="radio" name="thing" value="number" checked></label>
  <label>Letter&nbsp;<input type="radio" name="thing" value="letter"></label>
</div>
<textarea id="debug" rows=5 cols=60 placeholder="Ssshh, don't tell anyone about this secret debug area"></textarea>
AKX
  • 152,115
  • 15
  • 115
  • 172
  • This could be useful. Is there a way to shuffle a copy of the array, rather than the actual thing itself? My generator also has an ordered mode, where it displays one item at a time from top to bottom. I'm worried that shuffling the array would ruin the 'ordered' mode. – guyw Jul 28 '20 at 19:06
  • Shallow-copy the array with e.g. `const copiedArray = [...array];`, then shuffle it. – AKX Jul 28 '20 at 19:07
  • I'm afraid I couldn't get this to work. Could you edit your answer to include a snippet? – guyw Jul 28 '20 at 19:13
  • 1
    @guyw Added an example. :) – AKX Jul 28 '20 at 19:27
1

If you don't want to alter your original array by shuffling it, you can store the unused items in a separate array and then reset it when you're out. See the comments:

var oddnumber = [
  '111',
  '222',
  '333',
  '444',
  '555',
]
var oddletter = [
  'AAA',
  'BBB',
  'CCC',
  'DDD',
  'EEE',
]

// to store unused items
var unused_letters = [];
var unused_numbers = [];

function newThing() {
  if (numberCheck.checked) {
    // if there are no unused items, copy them form the source array
    if (!unused_numbers.length) unused_numbers = [...oddnumber];
    var randomY = unused_numbers;
  }
  if (letterCheck.checked) {
    // if there are no unused items, copy them form the source array
    if (!unused_letters.length) unused_letters = [...oddletter];
    var randomY = unused_letters;
  }
  var randomX = Math.floor(Math.random() * (randomY.length));

  var y = randomY;
  var x = randomX;
  document.getElementById("thingDisplay").innerHTML = y[x];

  // remove randomx from the unused array since it's been used now
  randomY.splice(randomX, 1);
}
<body>
  <div id='thingDisplay'></div>
  <div>
    <button class="box" id="button01" onclick="newThing()">New Thing</button>
  </div>
  <div>
    <form>
      Number<label>&nbsp;<input type="radio" name="thing" id="numberCheck"/></label>
      <br/>Letter<label>&nbsp;<input type="radio" name="thing" id="letterCheck"/></label>
    </form>
  </div>
</body>
I wrestled a bear once.
  • 22,983
  • 19
  • 69
  • 116
1

Well, as I said earlier all you have to do is to

  1. Create a copy from your current array
  2. Select one element from the copied array
  3. Pop the previously selected item
  4. Choose from remaining ones
  5. Do this until all items get exhausted
  6. Do all of these all over again (get back to step 1)

That's all that you need to produce a random index without repetition. So besides the algorithm part, since the main function contains a closure (It will magically remember the previous value of the sliced array) for algorithm part, you need to invoke the main function only once when the radio check gets change, so you need to listen to radio change event to invoke the main function containing the algorithm closure. Then to avoid the unobtrusive effect instead of using onclick on the HTML element, please use the addEventListener method for the button click event.

So your final code should be something like this:

var chooser = null;
var radios = document.querySelectorAll('input[type=radio]');
var button = document.getElementById('button01');
var oddnumber = ['111', '222', '333', '444', '555'];
var oddletter = ['AAA', 'BBB', 'CCC', 'DDD', 'EEE'];

function randomNoRepeats(array) {
  var copy = array.slice(); // Create a copy of input array
  return function() {
    if (copy.length < 1) { // This line exist to create copy and make a new array from actual array whenever all possible options are selected once
      copy = array.slice();
    }
    var index = Math.floor(Math.random() * copy.length); // Select an index randomly
    var item = copy[index]; // Get the index value
    copy.splice(index, 1); // Remove selected element from copied array
    return item; // Return selected element
  };
}

Array.from(radios).forEach(radio => { // Listening to all radio inputs change events
  radio.addEventListener('change', function() {
    if (numberCheck.checked) {
      chooser = randomNoRepeats(oddnumber);
    }
    if (letterCheck.checked) {
      chooser = randomNoRepeats(oddletter);
    }
  });
});

button.addEventListener('click', function() { // Listen to submit button click
  document.getElementById("thingDisplay").innerHTML = chooser();
});
<div id='thingDisplay'></div>
<div>
  <button class="box" id="button01">New Thing</button>
</div>
<div>
  <form>
    Number<label>&nbsp;<input type="radio" name="thing" id="numberCheck"/></label>
    <br/>Letter<label>&nbsp;<input type="radio" name="thing" id="letterCheck"/></label>
  </form>
</div>
SMAKSS
  • 9,606
  • 3
  • 19
  • 34