Функция math.random в Lua, случайный выбор, дроп с весами, перемешивание
Опубликовано:
- Генерация случайных чисел
- Случайный выбор элемента из таблицы
- Чем выше "вес" элемента, тем чаще выпадает
- Перемешивание массива
Функция math.random в Lua
Генерация случайных чисел в Lua выполняется с помощью функции random() из библиотеки math, которая работает в трех режимах:
math.random()— возвращает вещественное число от 0 до 1.math.random(max)— возвращает целое число от 1 до max.math.random(min, max)— возвращает целое число в заданном диапазоне.
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, ", "))
Исходная колода: Король, Дама, Валет, Туз, Десятка, Девятка Перемешанная колода: Девятка, Туз, Король, Дама, Десятка, Валет
Исходная колода: Король, Дама, Валет, Туз, Десятка, Девятка Перемешанная колода: Десятка, Валет, Туз, Король, Девятка, Дама