something like this is a bit expensive to set up, but minting is cheap, so I think it's along the lines of what you want to do.
Rather than keeping track of what's been minted already, it keeps a set of IDs that haven't been minted and selects from that. The set gets smaller as NFTs are minted.
pragma solidity ^0.8.7
import "hardhat/console.sol";
contract RandomMints {
uint[] notMinted;
function createSet() external {
// this is a bit expensive but a one off
// txn gas 2282618 in remix
for(uint i; i < 100; ++i){
notMinted.push(i);
}
}
function pickId() external returns (uint) {
// txn gas 37430 in remix
uint numLeft = notMinted.length;
console.log("num of NFTs not minted:", numLeft);
// pick a number between 0 - num of unminted NFTs -1
uint rand = block.timestamp % (numLeft - 1); //not really random
uint idSelected = notMinted[rand];
// now reduce the set. it doesn't matter if we mix up the order as it adds more entropy
// get rid of the picked id by overwriting it with the last id/element
notMinted[rand] = notMinted[numLeft -1];
// pop the last (now duplicated) id/element off the array
notMinted.pop();
console.log("id selected:", idSelected);
return idSelected;
}
}