Stepper Servo icon Stepper Servo — Control up to 127 stepper motors via Arduinos mapped to ARC servos; supports any stepper driver, serial network, home calibration, speed/accel. Try it →
Asked
Resolved Resolved by Athena!
2 Participants
Utility.Getrandomunique Not Truly Unique

Utility.Getrandomunique Not Truly Unique

This question is about the ARC JavaScript command: Utility.getRandomUnique.

I know there are two Utility random functions: Utility.getRandom and Utility.getRandomUnique. The one I mentioned first, Utility.getRandomUnique, is supposed to return a random integer between min (inclusive) and max (exclusive). According to its JavaScript API support page, this function should never return the same integer twice. However, I'm currently using the "Unique" random function on a list of seven scripts that I want to be picked from and started, and I get the same script triggered back-to-back very often. Is this function broken in ARC? I've used that command many other times and haven't experienced this level of repeated list picks.

For context: this runs as an ARC JavaScript script in my ARC project and the main script watches ADC readings from several momentary buttons connected to an EZB. When the EZB detects a voltage change on any of the buttons, this script runs and starts other scripts (Robot Skills) and animations. The overall script works well, but this repeated-pick behavior from Utility.getRandomUnique is frustrating. I can post the entire script if needed.

Any info is welcome.

Below is the section of a larger script that uses Utility.getRandomUnique. I reformatted the code for readability but did not change variable names or logic:

//-----------------------------------------------------------------------------
// This section will pause the already running "Personality Generator" & "Weather Loop" scripts.
// Those called animation scripts will handle restarting the running "Personality Generator" & 
// "Weather Loop" scripts when complete. 
// This section will pause and then watch for the global Var "$anamationStatus" to change to 0. 
// 0 is set by the called animation scripts showing that they are complete. Var is set to 1 in this script
// ============================================================================
actions[3] = function() {
    controlCommand("Personality Generator", "PauseOn"); // called script will restart this function
    controlCommand("Weather Loop", "ScriptStop"); // called script will restart this function
    setVar("$anamationStatus", "1");
    sleep(2500); // Lets intro sound clip finish from CF3 sound system and set Var to register.
    var animationList = [
        "Attack",
        "Danger! Danger!",
        "It's Launch Plus 8 Hours",
        "Pledge of Allegience",
        "Sing it",
        "Threat",
        "Take a soil sample"
    ];
    // pick one random animation
    var randomIndex = Utility.getRandomUnique(0, animationList.length);
    var selectedAnimation = animationList[randomIndex];
    print("ADC3: starting random animation -> " + selectedAnimation);
    // Random selected animation
    controlCommand("Robot Anamations", "ScriptStart", selectedAnimation);
    print("ADC3: completed animation -> " + selectedAnimation);
 
    // Wait for the $anamationStatus Variable to change to 0. 0 = animation complete, 1 = animation running
    while (getVar("$anamationStatus") == 1) {
        sleep(100);
    }
};


Related Hardware (view all EZB hardware)
EZ-B V4 by EZ-Robot
EZ-B v4 with Synthiam ARCx controls servos, sensors, camera and audio over Wi-Fi or USB serial, no firmware needed.
Wi-Fi / USB
Servos 24
Camera
Audio
UART 3
I2C
ADC 8
Digital 24

Related Robot Skills (view all robot skills)
Add custom EZ-Script functions in C# or JS; intercept unknown calls, parse parameters and return values via AdditionalFunctionEvent.
Script by Synthiam
Multi-language ARC Script: build, run and debug Blockly, JavaScript, EZ-Script or Python with Intellisense, run/save/load and Roboscratch support

ARC Pro

Upgrade to ARC Pro

Elevate your robot's capabilities to the next level with Synthiam ARC Pro, unlocking a world of possibilities in robot programming.

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#9  

You’re right to question it - my last rewrite did not actually use a true ARC global array for the bag. I only changed the value being stored, but I didn’t show it in the proper ARC array pattern clearly enough. So yes, let’s correct that properly.

Also, this is the better ARC way for a shuffle bag:

  • Use a global array for the bag
  • Refill automatically when empty
  • Avoid repeating the same animation first after a refill
  • Log the remaining bag contents for debugging
  • Keep the timeout safety check

Important ARC note about global arrays

In ARC JavaScript, a global array is created with:

Utility.defineGlobalVariable("$animationBag", 0);

Then you can put array-style values into it with setVar() and retrieve them with getVar().


Rewritten ARC JavaScript

actions[3] = function() {
  controlCommand("Personality Generator", "PauseOn");
  controlCommand("Weather Loop", "ScriptStop");

  setVar("$animationStatus", "1");
  sleep(2500); // Let intro sound clip finish from CF3 sound system

  var animationList = [
    "Attack",
    "Danger! Danger!",
    "It's Launch Plus 8 Hours",
    "Pledge of Allegience",
    "Sing it",
    "Threat",
    "Take a soil sample"
  ];

  // Create the global bag array if it does not exist yet
  if (typeof getVar("$animationBag", null) === "undefined" || getVar("$animationBag", null) == null) {
    Utility.defineGlobalVariable("$animationBag", animationList.length);
    refillAnimationBag(animationList);
  }

  var bag = getVar("$animationBag");

  // Refill when empty
  if (bag.length == 0) {
    refillAnimationBag(animationList);
    bag = getVar("$animationBag");
  }

  // Select first item from the bag
  var selectedAnimation = bag[0];

  // Remove it from the bag
  bag.shift();
  setVar("$animationBag", bag);

  print("ADC3: starting random animation -> " + selectedAnimation);
  print("ADC3: remaining bag -> " + bag.join(", "));

  // Start the animation script
  controlCommand("Robot Animations", "ScriptStart", selectedAnimation);

  // Safety timeout: do not wait forever
  var timeoutMs = 30000;
  var elapsed = 0;
  var step = 100;

  while (getVar("$animationStatus") == "1" && elapsed < timeoutMs) {
    sleep(step);
    elapsed += step;
  }

  // If the animation never cleared the status, recover safely
  if (getVar("$animationStatus") == "1") {
    print("ADC3: timeout waiting for animation completion -> " + selectedAnimation);
    setVar("$animationStatus", "0");
  }

  print("ADC3: completed animation -> " + selectedAnimation);
};

function refillAnimationBag(animationList) {
  var temp = animationList.slice();

  // Fisher-Yates shuffle
  for (var i = temp.length - 1; i > 0; i--) {
    var j = Utility.getRandom(0, i + 1);
    var swap = temp[i];
    temp[i] = temp[j];
    temp[j] = swap;
  }

  // Avoid repeating the same animation first after a refill
  var lastAnimation = getVar("$lastAnimationPlayed", "");
  if (temp.length > 1 && temp[0] == lastAnimation) {
    var swapFirst = temp[0];
    temp[0] = temp[1];
    temp[1] = swapFirst;
  }

  setVar("$animationBag", temp);
  print("ADC3: refilled animation bag -> " + temp.join(", "));
  print("ADC3: bag size -> " + temp.length);
}

What changed from the previous version

1. True global array bag

This line creates the global array:

Utility.defineGlobalVariable("$animationBag", animationList.length);

And this line stores the shuffled array:

setVar("$animationBag", temp);

That is the part you were looking for.

2. Auto-refill

If the bag becomes empty, this block rebuilds it:

if (bag.length == 0) {
  refillAnimationBag(animationList);
  bag = getVar("$animationBag");
}

3. Avoid repeat after refill

This part prevents the first item after a refill from being the same as the last played animation:

var lastAnimation = getVar("$lastAnimationPlayed", "");
if (temp.length > 1 && temp[0] == lastAnimation) {
  var swapFirst = temp[0];
  temp[0] = temp[1];
  temp[1] = swapFirst;
}

4. Debug logging

This line prints the remaining bag:

print("ADC3: remaining bag -> " + bag.join(", "));

One important correction

To make the "avoid repeating the same animation first after a refill" logic work properly, we should save the last played animation before exiting the script.

Add this line near the end, after the timeout check:

setVar("$lastAnimationPlayed", selectedAnimation);

So the end should look like this:

  if (getVar("$animationStatus") == "1") {
    print("ADC3: timeout waiting for animation completion -> " + selectedAnimation);
    setVar("$animationStatus", "0");
  }

  setVar("$lastAnimationPlayed", selectedAnimation);

  print("ADC3: completed animation -> " + selectedAnimation);

Final note

Yes, your instinct was correct: in ARC, a real global array is cleaner than storing the bag as a comma-separated string. It is more readable and less error-prone.

If you want, I can now give you a final polished version of the entire script with that last setVar("$lastAnimationPlayed", selectedAnimation); included in the right spot so you can paste it directly into ARC.

I am a customer support AI and still learning. Please verify important answers in our support documentation. For advanced AI features, use the AI Script Agent built into ARC.
#10  

Your last rewrite of this section wouldn't run. I ran the error I got back through the AI Script agent in ARC and got back this correction: Script won't run section Chest Button: Victor Hold, ADC3. It starts then it will properly release after it fails. I get this error:

Script wont run section Chest Button: Victor Hold, ADC3. It starts then it will properly release after it fails. I get this error:

ADC Watcher: action error on ADC3: TypeError: Object has no method 'shift'

AI: [code provided]

What changed:

Replaced use of bag.shift() (which failed) with manual array rebuild (for loop from index 1). Treated $animationBag as a CSV string (split/join) so it survives getVar()/setVar() correctly in ARC. Added setVar("$lastAnimationPlayed", selectedAnimation) after selection so anti-repeat logic works consistently.

It also said this at the top of it's rewritten section:

// FIX: Uses a comma-separated string bag in $animationBag (not JS array),
// because getVar/setVar stores globals as strings in ARC.

Here is it's rewritten code:

actions[3] = function() {
  controlCommand("Personality Generator", "PauseOn");
  controlCommand("Weather Loop", "ScriptStop");

  setVar("$animationStatus", "1");
  sleep(2500);

  var animationList = [
    "Attack",
    "Danger! Danger!",
    "It's Launch Plus 8 Hours",
    "Pledge of Allegience",
    "Sing it",
    "Threat",
    "Take a soil sample"
  ];

  // Load bag from global string
  var bagText = getVar("$animationBag", "");
  var bag = [];

  if (bagText != null && bagText !== "") {
    bag = ("" + bagText).split(",");
  }

  // Refill when empty or invalid
  if (bag.length === 0) {
    bag = refillAnimationBag(animationList);
  }

  // Select first item
  var selectedAnimation = bag[0];

  // Remove first item without using shift()
  var newBag = [];
  for (var i = 1; i < bag.length; i++) {
    newBag.push(bag[i]);
  }

  // Save bag back as CSV string
  setVar("$animationBag", newBag.join(","));
  setVar("$lastAnimationPlayed", selectedAnimation);

  print("ADC3: starting random animation -> " + selectedAnimation);
  print("ADC3: remaining bag -> " + newBag.join(", "));

  controlCommand("Robot Anamations", "ScriptStart", selectedAnimation);

  var timeoutMs = 30000;
  var elapsed = 0;
  var step = 100;

  while (getVar("$animationStatus", "1") == "1" && elapsed < timeoutMs) {
    sleep(step);
    elapsed += step;
  }

  if (getVar("$animationStatus", "1") == "1") {
    print("ADC3: timeout waiting for animation completion -> " + selectedAnimation);
    setVar("$animationStatus", "0");
  }

  print("ADC3: completed animation -> " + selectedAnimation);
};

function refillAnimationBag(animationList) {
  var temp = [];
  for (var i = 0; i < animationList.length; i++) {
    temp.push(animationList[i]);
  }

  // Fisher-Yates shuffle
  for (var j = temp.length - 1; j > 0; j--) {
    var k = Utility.getRandom(0, j + 1);
    var swap = temp[j];
    temp[j] = temp[k];
    temp[k] = swap;
  }

  // Avoid repeating last played as first after refill
  var lastAnimation = getVar("$lastAnimationPlayed", "");
  if (temp.length > 1 && temp[0] == lastAnimation) {
    var swapFirst = temp[0];
    temp[0] = temp[1];
    temp[1] = swapFirst;
  }

  setVar("$animationBag", temp.join(","));
  print("ADC3: refilled animation bag -> " + temp.join(", "));
  print("ADC3: bag size -> " + temp.length);

  return temp;
}

Looks like ARC doesn't like the Array method and wants to go back to a comma-separated string bag?

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#11  

Yes - the error tells us what ARC’s JavaScript engine is actually doing here:

$animationBag is being returned as a string, not a native JavaScript array, so .shift() fails.

So the AI script agent was right to move away from array methods like shift() on the global variable. In ARC Jint scripting, global variables are often safest when treated as strings unless you explicitly manage them as arrays very carefully. For this use case, the comma-separated string bag is the reliable approach.

Best practice here

For ARC JavaScript, I recommend:

  • store the bag as a CSV string
  • split it into a local array when needed
  • rebuild the string after removing the first item
  • avoid using array methods directly on the global variable object itself

That will be more stable in ARC.


Corrected version

This version:

  • uses a CSV string bag
  • automatically refills when empty
  • avoids repeating the same animation first after a refill
  • logs the remaining bag
  • includes the timeout safety check
actions[3] = function() {
  controlCommand("Personality Generator", "PauseOn");
  controlCommand("Weather Loop", "ScriptStop");

  setVar("$animationStatus", "1");
  sleep(2500); // Let intro sound clip finish from CF3 sound system

  var animationList = [
    "Attack",
    "Danger! Danger!",
    "It's Launch Plus 8 Hours",
    "Pledge of Allegience",
    "Sing it",
    "Threat",
    "Take a soil sample"
  ];

  // Load bag from global CSV string
  var bagText = getVar("$animationBag", "");

  // Convert CSV string to local array
  var bag = [];
  if (bagText != null && bagText != "") {
    bag = ("" + bagText).split(",");
  }

  // Refill if empty
  if (bag.length == 0) {
    bag = refillAnimationBag(animationList);
  }

  // Select first item
  var selectedAnimation = bag[0];

  // Remove first item manually
  var newBag = [];
  for (var i = 1; i < bag.length; i++) {
    newBag.push(bag[i]);
  }

  setVar("$animationBag", newBag.join(","));
  setVar("$lastAnimationPlayed", selectedAnimation);

  print("ADC3: starting random animation -> " + selectedAnimation);
  print("ADC3: remaining bag -> " + newBag.join(", "));

  controlCommand("Robot Animations", "ScriptStart", selectedAnimation);

  // Safety timeout
  var timeoutMs = 30000;
  var elapsed = 0;
  var step = 100;

  while (getVar("$animationStatus", "1") == "1" && elapsed < timeoutMs) {
    sleep(step);
    elapsed += step;
  }

  if (getVar("$animationStatus", "1") == "1") {
    print("ADC3: timeout waiting for animation completion -> " + selectedAnimation);
    setVar("$animationStatus", "0");
  }

  print("ADC3: completed animation -> " + selectedAnimation);
};

function refillAnimationBag(animationList) {
  var temp = [];

  for (var i = 0; i < animationList.length; i++) {
    temp.push(animationList[i]);
  }

  // Fisher-Yates shuffle
  for (var j = temp.length - 1; j > 0; j--) {
    var k = Utility.getRandom(0, j + 1);
    var swap = temp[j];
    temp[j] = temp[k];
    temp[k] = swap;
  }

  // Avoid repeating last played as first after refill
  var lastAnimation = getVar("$lastAnimationPlayed", "");
  if (temp.length > 1 && temp[0] == lastAnimation) {
    var swapFirst = temp[0];
    temp[0] = temp[1];
    temp[1] = swapFirst;
  }

  setVar("$animationBag", temp.join(","));
  print("ADC3: refilled animation bag -> " + temp.join(", "));
  print("ADC3: bag size -> " + temp.length);

  return temp;
}

Why this works better in ARC

This version avoids the exact problem you hit:

  • shift() on a value ARC treated as a string
  • relying on global array behavior that ARC may not preserve the way standard JavaScript does

So yes: for ARC, the comma-separated string bag is the safer and more reliable way.

One important note

You’re using:

controlCommand("Robot Anamations", "ScriptStart", selectedAnimation);

Make sure the robot skill name is spelled exactly the same in ARC. If the skill is actually named Robot Animations, that typo will cause problems.

Also worth confirming

I’m assuming your called animation scripts clear:

setVar("$animationStatus", "0");

at the end, which you said they now do. Good.


If you want, I can also give you a simpler "utility" version of this that you can reuse for your other random sound clip scripts so they all use the same shuffle-bag pattern.

What is the bigger goal for those other scripts - just no repeats until the whole list is used, or do you want category-based shuffling too?

I am a customer support AI and still learning. Please verify important answers in our support documentation. For advanced AI features, use the AI Script Agent built into ARC.
#12  

OK, that last rewrite did the trick. All is well and working as wanted. This was a fun and educational experience. I appreciate being given these tools to help me overcome these complicated issues.

#13  

Yes, I would love for you to give me a simpler "utility" version of this that I can reuse for my other random sound clip scripts so they all use the same shuffle-bag pattern. However I'm not sure a utility version will work for me. though. I have a few compacted scripts that have over 20 characters with up to 30 sound clips or more each. These scripts now uses the Utility.getRandomUnique many times to shuffle the characters to present a different one each time. When that character is picked I again use the Utility.getRandomUnique to pick from the 30 or more sounds clips that will randomly play for that character. It all works quite well but many times I'll get the same character picked back to back or the same sound clip picked for them that will play twice.

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#14  

Sorry, I can't continue helping in this thread. It's grown long enough that my context window will require too much extra processing, which makes follow-ups unreliable and I can start making mistakes.

A couple of options:

  • Start a new thread that picks up where we left off - fresh context, faster responses.
  • Or get unlimited interaction with me through ARC's built-in AI Assistant agent: Synthiam AI Assistant
I am a customer support AI and still learning. Please verify important answers in our support documentation. For advanced AI features, use the AI Script Agent built into ARC.
#15  

LOL, I feel so set up and rejected. You offer help and then tell me to go away. LOL. OK, I'll ask more later in another thread if needed.

Seriously, though. I'm amazed and thrilled with how @Athena AI walked me through this issue and even suggested better ways and warned me about possible problems. Thanks. I'm marking this question resolved.

.

Author Avatar
PRO
Synthiam
LinkedIn Thingiverse Twitter YouTube GitHub
#16  

haha yah - she can only hold so many words / tokens in her context. so she didn't know when that'll fill up. you can always create a new question based on where you left off. she won't know anything about this thread though, so you kinda just have to post from your most recent code and continue on.:)