G'day!

我被另一个Reddit用户问到这个问题,所以我想分享一下我的方法,希望它能对其他人有用。 在我的游戏中,你需要从字母表中的字母网格中拼出单词,这与《书虫冒险》类似。但是,这种方法可以适用于任何时候你想简单公平地进行加权随机抽取。

如果我只随机挑选一个字母,我很容易得到不可用的网格,字母全部是Z、X和W。 我的第一个方法是生成单词列表的频率分布。这只是从所有单词中遍历,遇到每个字母时,我增加一个对应的计数,对应的计数就是一个字母的数组中的一个条目。 这样你就清楚地知道单词列表中A比Z的出现频率有多高。 我在事先完成了此项工作,用保存频率数据到CSV文件的方式。 在此刻的阶段, 我也预先计算了一些其他的内容,如从原始频率转换为更直接的可用数据。

运行时,我有一个TileBag类,这只是字母对的数组。使用频率分布,我将每个字母多次添加到TileBag中。最后, 我随机打乱了这个数组。随着我需要字母的时候,我只是拿最后一个字母。 当TileBag为空时,我使用相同的逻辑来重新填充它。

以往我为“公平”的掷骰子使用了这种方法。想掷一个完全公平的20面骰子吗?将骰点数添加到袋子中,然后打乱并抽取。玩家得到一个完全公平的结果, 1 一样可能出现于随机结果的100分之一为 20。 如果玩家具有某种优点/劣势,你可以使用这些优点/劣势来为他们增加/减少抽奖的权重。

以下是我的TileBag代码,这是用GDScript写的, 但它很容易被转移到任何语言中。

class_name TileBag
extends RefCounted

const DEBUG_LOG := false

var tiles : Array[String] = []
var letter_frequency : LetterFrequency
var multiplier : int


func _init(p_letter_freq : LetterFrequency, p_multiplier : int) -> void:
    letter_frequency = p_letter_freq
    multiplier = p_multiplier
    var tw := p_letter_freq.tile_weights
    for letter in tw:
        var weight := int(tw[letter])
        if DEBUG_LOG: printt(letter, tw[letter], weight)
        for _i in range(weight * p_multiplier):
            tiles.append(letter)

    #print(tiles)
    tiles.shuffle()


func draw_tile() -> String:
    assert(not tiles.is_empty())
    var tile : String = tiles.pop_back()

    return tile


func size() -> int:
    return tiles.size()


func is_empty() -> bool:
    return tiles.is_empty()


func clear() -> void:
    tiles.clear()


## 
func refill() -> void:
    if DEBUG_LOG: print("Refilling bag, it only has %d tile left" % size())
    var tw := letter_frequency.tile_weights
    for letter in tw:
        var weight := int(tw[letter])
        if DEBUG_LOG: printt(letter, tw[letter], weight)
        for _i in range(weight * multiplier):
            tiles.append(letter)

    tiles.shuffle()

你可以在以下位置看到我的游戏Lexomancer:

https://muffinmangames.itch.io/lexomancer-alpha