The probability of collisions is low. So just hope that there are no collisions, and try again if there are. You can detect collisions because zmv has a specific error message for that.
attempts=0 while ((++attempts)); err=$(zmv … 2>&1); [[ $# -eq 1 && $err == *" both map to "* && $attempts -le 10 ]]; do print -lr -- $err >&2 done [[ -z $err ]]
The limit to the number of attempts is both in case there's a mistake and the renaming has a high chance of collisions, and in case some other error occurs but the string both map to appears in a file name.
Here are some other approach which are more complex than needed for the given problem, but demonstrate the use of zmv outside its comfort zone. The zmv interface is primarily designed for situations where the method to calculate the replacement of each file name is independent of the other file names. However, there are a few tricks to have dependencies between replacement names.
One thing that's easy is to inject a counter into the replacement. You can increment the counter in an arithmetic expression. This survives from one file name to the next because the replacement expression is not evaluated in a subshell.
function rename_files_to_numbers { local i=0 # Preserve a single extension if any zmv $1 '$f:h/$((++i))${${f:e}:+.$f:e}' }
Running arbitrary code in the replacement expression is easy: it's just command substitution. But that puts the code in a subshell, so you can't keep arbitrary state around between calls. However, for this specific task, all you need is the internal variable from that zmv maintains to keep track of the new file names. I assume that file names don't end with a newline.
zmv '*(.*)' '$(n=$(randomname)$1; while [[ -n $from[$n] || -e $n ]]; do n=$(randomname)$1; done; print -r -- $n)'
The from variable is undocumented, although it's been there ever since zmv was introduced. What if we didn't want to rely on it? Then it's more complicated, but we can still update state through ${VAR::=VALUE} parameter substitutions. The code below uses n as a temporary variable for the new name and seen[$n] keeps track of new file names that are already allocated. ${…:+} hides the result of a nested parameter expansion.
unset seen; typeset -A seen zmv '*(.*)' '${n::=$(n=$(randomname)$1; while [[ -n $seen[$n] || -e $n ]]; do n=$(randomname)$1; done; print -r -- $n)}${${seen[$n]::=1}:+}'
A less intuitive approach that leads to simpler code is to prepare the replacement text as soon as you do the pattern matching. Within the pattern, you can use the e glob qualifier to run arbitrary code.
function assign_new_name { n=$(randomname).${REPLY:e}; while [[ -n $seen[$n] || -e $n ]]; do n=$(randomname).${REPLY:e}; done; seen[$n]=1; map[$REPLY]=$n; } unset seen map; typeset -A seen map; zmv -nv -Q '*.*(+assign_new_name)' '$map[$f]'