Функция math.random в Lua, случайный выбор, дроп с весами, перемешивание

Опубликовано:

Функция math.random в Lua

Генерация случайных чисел в Lua выполняется с помощью функции random() из библиотеки math, которая работает в трех режимах:

for i = 1, 10 do
    local n = math.random()
    io.write(string.format('%.2f', n), ' ')
end
io.write('\n')

for i = 1, 20 do
    local n = math.random(3)
    io.write(n, ' ')
end
io.write('\n')

for i = 1, 20 do
    local n = math.random(-1, 1)
    io.write(n, ' ')
end
io.write('\n')
0.44 0.62 0.08 0.65 0.70 0.89 0.97 0.38 0.86 0.63 
2 1 2 1 1 2 3 2 3 3 2 2 2 1 2 2 2 1 1 2 
0 0 -1 1 0 1 1 1 -1 1 1 0 1 0 -1 0 -1 -1 1 0

Чтобы при каждом запуске программы не выдавалась одинаковая последовательность чисел, в более ранних версиях необходимо было "вручную" инициализировать генератор: math.randomseed(os.time()). Начиная с версии Lua 5.4 это делать необязательно.

Случайный выбор элемента из таблицы (массива)

Эмуляция случайности важна, например, в играх. Самый простой и частый сценарий — из имеющегося списка предметов надо выбрать один случайным образом. Для этого можно написать собственную функцию, в теле которой в диапазоне длинны массива генерируется случайное число, которое используется как индекс для взятия элемента.

local items = {"Яблоко", "Банан", "Апельсин", "Груша", "Ананас"}

-- Функция для выбора случайного элемента
local function getRandomElement(list)
    if #list == 0 then return nil end -- Проверка на пустую таблицу
    
    local randomIndex = math.random(#list)
    return list[randomIndex]
end

print("Выпавший фрукт:", getRandomElement(items))
Выпавший фрукт: Груша

Таблица дропа с весами (Weighted Random)

В играх часто необходима система лута, при которой чем выше "вес" предмета, тем чаще он выпадает откуда-либо.

Для ее реализации обычно используют алгоритм, работающий через кумулятивную (накапливаемую) сумму: считается общий вес, генерируется случайное число от 1 до этого максимума и далее проверяется, в какой диапазон попало число.

-- Список предметов и их шансов (весов)
local dropTable = {
    { name = "Обычный меч",   weight = 70 }, -- 70% шанс
    { name = "Редкий щит",    weight = 20 }, -- 20% шанс
    { name = "Эпический лук", weight = 9  }, -- 9% шанс
    { name = "Легендарный посох", weight = 1 } -- 1% шанс
}

local function getWeightedRandom(lootTable)
    -- Считаем общий вес всех предметов
    local totalWeight = 0
    for _, item in ipairs(lootTable) do
        totalWeight = totalWeight + item.weight
    end

    -- Генерируем случайное число в пределах общего веса
    local roll = math.random(1, totalWeight)

    -- Ищем, какому предмету принадлежит это число
    local currentWeight = 0
    for _, item in ipairs(lootTable) do
        currentWeight = currentWeight + item.weight
        if roll <= currentWeight then
            return item.name
        end
    end
end

-- Тест: симулируем 5 открытий сундука
print("--- Открытие сундуков ---")
for i = 1, 5 do
    print("Сундук " .. i .. ": " .. getWeightedRandom(dropTable))
end
--- Открытие сундуков ---
Сундук 1: Редкий щит
Сундук 2: Обычный меч
Сундук 3: Эпический лук
Сундук 4: Обычный меч
Сундук 5: Обычный меч

Может показаться, что лишним было вводить totalWeight, ведь известно, что суммарный шанс соответствует 100%. Также можно было бы не использовать currentWeight, а сделать множественную проверку на попадание в диапазоны. Однако в последствии это привело бы к сложностям при внесении изменений в код.

Так можно ошибиться при формировании таблицы весов, и там в сумме будет не 100. Кроме этого, в играх часто нужно временно менять шансы. Например, если у игрока выпито "зелье удачи", вес легендарного посоха должен вырасти с 1 до 5. Сумма станет 104, но числа 101–104 никогда не выпадут в math.random(1, 100). Но если totalWeight считается динамически, он адаптируется к этому автоматически.

Проверка фиксированных диапазонов (например, от 1 до 70, от 71 до 90 и так далее) страдает примерно теми же проблемами. Надо помнить границы, может нарушиться связанность данных. Если изменится шанс одного предмета, придется переписывать остальной код, так как сдвинутся границы других диапазонов.

Конструкция с currentWeight = currentWeight + item.weight выполняет такую же проверку диапазонов, но динамически в цикле. Она превращает веса предметов в виртуальную "линейку", где каждый предмет занимает отрезок, равный его весу.

Предмет Вес (шанс) Диапазон на «линейке»
⚔️ Обычный меч 70 от 1 до 70
🛡️ Редкий щит 20 от 71 до 90
🏹 Эпический лук 9 от 91 до 99
✨ Легендарный посох 1 100

В этом случае разработчику достаточно просто менять цифры весов в таблице dropTable, а код сам перестроит все диапазоны "на лету" без необходимости править функцию.

Перемешивание массива (алгоритм Фишера-Йетса)

Алгоритм Фишера-Йетса (Fisher-Yates shuffle) является простым и производительным способом перемешать колоду карт или список игроков. Проход идет с конца массива к началу, при этом текущий элемент меняется местами со случайным элементом, стоящим перед ним.

local deck = {"Король", "Дама", "Валет", "Туз", "Десятка", "Девятка"}

local function shuffleTable(list)
    -- Идем с конца таблицы до второго элемента
    for i = #list, 2, -1 do
        -- Выбираем случайный индекс от 1 до i
        local j = math.random(1, i)
        -- Меняем элементы местами (классический swap в Lua)
        list[i], list[j] = list[j], list[i]
    end
    return list
end

print("Исходная колода: ", table.concat(deck, ", "))
shuffleTable(deck)
print("Перемешанная колода:", table.concat(deck, ", "))
Исходная колода:        Король, Дама, Валет, Туз, Десятка, Девятка
Перемешанная колода:    Девятка, Туз, Король, Дама, Десятка, Валет
Исходная колода:        Король, Дама, Валет, Туз, Десятка, Девятка
Перемешанная колода:    Десятка, Валет, Туз, Король, Девятка, Дама