seed <- 123456789           # replace: draw a fresh number once, e.g. sample.int(1e9, 1) with no seed set
set.seed(seed)
groups <- c("vehicle", "low", "mid", "high")
animals <- data.frame(tag = sprintf("T%03d", 1:24), cage = rep(1:6, each = 4))   # or: read.csv("my_animals.csv"), columns tag, cage
key <- do.call(rbind, lapply(split(animals, animals$cage), function(a) {
  stopifnot(nrow(a) == length(groups))                # every group once per cage
  a$group <- sample(groups); a }))
key$code <- sample(sprintf("A%02d", seq_len(nrow(key))))   # codes reveal nothing
write.csv(key, "allocation_key.csv", row.names = FALSE)    # move to the key-holder's own storage at once
write.csv(key[, c("cage", "tag", "code")], "coded_sheet.csv", row.names = FALSE)
table(key$cage, key$group)                            # key-holder's check: one of each group per cage
