首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >Razzle模拟器

Razzle模拟器
EN

Code Review用户
提问于 2019-06-09 17:22:14
回答 2查看 1.2K关注 0票数 16

受来自诈骗国家的视频和来自Numberphile的James的启发,我试图制作一个金酸莓模拟器。

“金酸莓”是一场以游戏形式出现的骗局。每轮,玩家支付一笔费用,并将8个弹珠扔到一块板子上,这样他们就会落在板子上的洞里。每个洞的分数从1到6分,掷8骰子也可以。将分数相加,形成一个从8到48的分数。这个分数通过表格/图表被转换成分数。积分是轮流累积的。当玩家达到100分时,它就赢得了奖品。当达到100分时,一些分数会增加奖品的数量。一个分数29倍的费用每轮,成倍,所以得分29倍的费用增加10倍,以1024倍的初始费用。

诀窍是最常见的分数(22-34分)不给任何分数。这意味着,只有2.7%的公平掷骰子投出分数,需要369.5转才能达到100点。对于视频中的董事会,只有0.28%的人给予分数,导致5000+转身得到100分。29分的概率约为8%,这将导致大量的费用,当玩很多次。

代码语言:javascript
复制
import random, numpy
import matplotlib.pyplot as plt

# return one int with random value [1,6], with the probability density described in rawMassDist
# every 1000 turns, sample 1000 loaded die throws and put them in a list
randoms = []
idxRandom = 0
def throwLoadedDie():
    global idxRandom
    global randoms
    rawMassDist = [11, 17, 39, 44, 21, 11]
    #rawMassDist = [50, 5, 5, 5, 5, 50]
    massDist = [float(i)/sum(rawMassDist) for i in rawMassDist]
    if (idxRandom % 1000) == 0:
        #randoms = numpy.random.choice(range(1, 7), size=1000, p=massDist)
        randoms = random.choices(range(1,7), massDist, k=1000)
        idxRandom = 0
    idxRandom += 1
    return randoms[idxRandom-1]

# throw 8 dice, fairDice indicates whether fair dice or loaded dice are used
# returns the sum of the dice values, which equals the score for this turn
def throwDice():
    total = 0
    for _ in range(0,8):
        if fairDice:
            total += random.randint(1,6);
        else:
            total += throwLoadedDie()
    return total

# translates the score into points using dictionary toPoints
def getPoints(score):
    toPoints = {8:100, 9:100, 10:50, 11:30, 12:50,
    13:50, 14:20, 15:15, 16:10, 17:5, 
    39:5, 40:5, 41:15, 42:20, 43:50, 
    44:50, 45:50, 46:50, 47:50, 48:100}
    if score in toPoints:
        return toPoints[score]
    return 0

# returns if this score results in an extra price
def isExtraPrize(score):
    if (18 <= score <= 21) or (score == 29) or (35 <= score <= 38):
        return True
    return False

# returns if this score doubles the fee for one turn
def needDoubleFee(score):
    return score == 29

# simulate one turn, return the new number of points, prizes and fee for the next turn
def simulateTurn(points, prizes, fee):
    score = throwDice()
    if isExtraPrize(score):
        prizes += 1
    if needDoubleFee(score):
        fee *= 2
    points += getPoints(score)
    return [points, prizes, fee, score]

# simulate single game, can result in win or loss in maxTurns turns
# can print result and histogram of scores
def playGame(printResult = True, maxTurns = 1000):
    points = 0
    prizes = 1
    hist = list() # start with empty list, add score after every turn
    hist2 = [0]*49 # entries 0-7 is always 0, other entries 8-48 represent the number of times a score has occurred
    fee = 1
    totalFee = 0
    goal = 100
    won = False
    for turn in range(1, maxTurns+1):
        #print('Turn {0}, points: {1}'.format(turn, points))
        totalFee += fee
        [points, prizes, fee, score] = simulateTurn(points, prizes, fee)
        hist.append(score)
        if points >= goal:
            won = True
            break

    # finalize
    [hist2, _] = numpy.histogram(hist, bins=49, range=[0,48])
    if printResult:
        if won:
            print('You win {0} prizes in {1} turns, cost: {2}'.format(prizes, turn, totalFee))
        else:
            print('You only got {0} points in {1} turns, cost: {2}'.format(points, turn, totalFee))

        print(hist2)
    if not won:
        prizes = 0
    return [prizes, turn, totalFee, hist2]

# simulate multiple games, allow many turns per game to practically ensure win
# also disable result printing in each game
def playGames(numGames, plot=False):
    hist = [0]*49
    totalPrizes = 0
    totalTurns = 0
    totalFee = 0
    withPoints = 0
    gamesLost = 0
    for i in range(0, numGames):
        [prizes, turns, fee, hist2] = playGame(False, 100000)
        if prizes == 0:
            gamesLost += 1
        hist = [x + y for x, y in zip(hist, hist2)]
        totalPrizes += prizes
        totalFee += fee
        totalTurns += turns
    for i in range(8, 18):
        withPoints += hist[i]
    for i in range(39, 49):
        withPoints += hist[i]
    print('{0} games, lost {1}'.format(numGames, gamesLost))
    print('Avg prizes: {}'.format(totalPrizes/numGames))
    print('Avg turns: {}'.format(totalTurns/numGames))
    print('Avg fee: {}'.format(totalFee/numGames))
    print(hist)
    print('Percentage turns with points: {:.2f}'.format(100.0*withPoints/sum(hist)))

    if plot:
        # create list of colors to color each bar differently
        colors = [item for sublist in [['red']*18, ['blue']*21, ['red']*10] for item in sublist]
        plt.bar(range(0, 49), hist, color=colors)
        plt.title('Score distribution across multiple games')
        plt.xlabel('Score = sum of 8 dice')
        plt.ylabel('Number of turns')
        plt.text(40, 0.6*max(hist), 'Red bars\ngive points')
        plt.show()

fairDice = False
#playGame()
playGames(100, plot=True)

具体问题:

  1. 由于调用random.choices()有一些开销,所以我生成了1000个加载的模具卷,并将其放入一个全局数组中。在没有课的情况下做这件事更好吗?在C语言中,我可能会使用静态变量。
  2. 要生成游戏期间所有分数的直方图,我会在每个回合中添加一个列表,然后生成直方图。这是高效的表现吗?
  3. 我的名字怎么样?特别是histhist2isExtraPrize()needDoubleFee()
  4. 我的Ryzen 52400G与3200 MHz内存需要大约15s来模拟100个加载游戏,平均。每局3550圈。我觉得这应该更快,任何与性能相关的建议都是受欢迎的。
  5. 当然,一般代码评审答案也是受欢迎的。
EN

回答 2

Code Review用户

回答已采纳

发布于 2019-06-09 23:41:25

首先,在Python中使用camelCase并不理想。对于变量和函数名,首选是snake_case。我会把它和我展示的任何重写代码一起使用。

我认为throw_dice可以改进一点。您正在检查函数中每次迭代一次的fair_dice值,而不是在开始时检查一次。就性能而言,这是可以忽略不计的,但这是不必要的,每个循环检查一次表明它是一个可以在循环中更改的值,而这里的情况并非如此。

有不同的方法来处理这个问题,这取决于您想要遵守的佩普有多近;但我将展示的两种方法都取决于使用条件表达式分配给函数。在PEP之后,您可以这样做:

代码语言:javascript
复制
def throw_loaded_die():
    return 1 # For brevity

# Break this off into its own function
def throw_fair_die():
    return random.randint(1, 6)

def throw_dice():
    # Figure out what we need first
    roll_f = throw_fair_die if fair_dice else throw_loaded_die

    total = 0
    for _ in range(8):
        total += roll_f() # Then use it here

    return total

这样就减少了复制,这很好。我还在调用0时去掉了range参数,因为如果没有指定它,这是隐式的。

不过,我认为单独的def throw_fair_die是不幸的。对于这样一个简单的函数,在任何地方都不需要,我发现它很吵,环顾四周,我不是只有一个人有这种感觉。就我个人而言,我宁愿写:

代码语言:javascript
复制
def throw_dice():
    # Notice the lambda
    roll_f = (lambda: random.randint(1, 6)) if fair_dice else throw_loaded_die

    total = 0
    for _ in range(8): # Specifying the start is unnecessary when it's 0
        total += roll_f()

    return total

不过,这可以说是一个“命名lambda”,这违反了佩普的建议:

始终使用def语句,而不是直接将lambda表达式绑定到标识符的赋值语句。

¯\_(ツ)_/

我仍然认为它可以改进。仔细看这圈。这只是一个总结循环!Python有一个内置的,可以与生成器表达式一起干净地使用:

代码语言:javascript
复制
def throw_dice():
    roll_f = throw_fair_die if fair_dice else throw_loaded_die

    return sum(roll_f() for _ in range(8))

is_extra_prize有一个冗余的返回。可将其简化为:

代码语言:javascript
复制
def is_extra_prize(score):
    return (18 <= score <= 21) or (score == 29) or (35 <= score <= 38)

不过,我要指出的是,在它下面有need_double_fee。要么将score == 29分割成它自己的函数(在这种情况下,它应该在适当的情况下使用),要么它是不正确的。如果您觉得有必要将它作为一个单独的功能,我将使用它:

代码语言:javascript
复制
def need_double_fee(score):
    return score == 29

def is_extra_prize(score):
    return (18 <= score <= 21) or need_double_fee(score) or (35 <= score <= 38)

尽管可以说,is_extra_prize中的其他两部分条件比score == 29更复杂,也可以通过将名称附加到它们而受益。还有一种选择是直接命名29魔术号,我认为这可能是一个更好的选择:

代码语言:javascript
复制
EXTRA_PRIZE_SCORE = 29

def is_extra_prize(score):
    return (18 <= score <= 21) or (score == EXTRA_PRIZE_SCORE) or (35 <= score <= 38)

您可能会发现命名18213538也是有益的;尽管这肯定会使该函数更加冗长。

我认为get_points也可以改进。分数字典似乎是“整个程序的成员”,而不是应该是功能局部的东西。还可以在字典中使用get来避免显式成员关系查找:

代码语言:javascript
复制
SCORE_TO_POINTS = {8:100, 9:100, 10:50, 11:30, 12:50,
                   13:50, 14:20, 15:15, 16:10, 17:5, 
                   39:5, 40:5, 41:15, 42:20, 43:50, 
                   44:50, 45:50, 46:50, 47:50, 48:100}

def get_points(score):
    # 0 is the default if the key doesn't exist
    return SCORE_TO_POINTS.get(score, 0)

simulate_turn返回一个元组(实际上是一个列表,虽然它可能应该是一个元组),表示游戏的新状态。对于简单的状态来说,这很好,但是当前的状态有四个部分,访问它们需要记住它们所处的顺序,如果数据放置的不正确,就允许出错。为了组织和清晰,或者甚至使用命名元组作为快捷方式,您可能希望在这里考虑使用类。

在相同的函数中,我还会在空间中添加一些行:

代码语言:javascript
复制
def simulate_turn(points, prizes, fee):
    score = throwDice()

    if isExtraPrize(score):
        prizes += 1

    if needDoubleFee(score):
        fee *= 2

    points += getPoints(score)

    return (points, prizes, fee, score)

个人风格,但我喜欢开放空间的代码。

您还可以消除参数的变异:

代码语言:javascript
复制
def simulate_turn(points, prizes, fee):
    score = throw_dice()

    return (points + get_points(score),
            prizes + 1 if is_extra_prize(score) else prizes,
            fee * 2 if need_double_fee(score) else fee,
            score)

虽然现在它已经写出来了,但我不知道我对它的感觉如何。

我真的只在这里和5.打交道。希望其他人能触及前四点。

票数 10
EN

Code Review用户

发布于 2019-06-10 14:54:43

最初的throwLoadedDie实现对每个调用执行一些不必要的计算,即

代码语言:javascript
复制
rawMassDist = [11, 17, 39, 44, 21, 11]
massDist = [float(i)/sum(rawMassDist) for i in rawMassDist]

是在每次通话中计算的。把它们移到if的S身上就像

代码语言:javascript
复制
if (idxRandom % 1000) == 0:
    rawMassDist = [11, 17, 39, 44, 21, 11]
    massDist = [float(i) / sum(rawMassDist) for i in rawMassDist]

在我的旧笔记本电脑上,计算时间从大约18秒减少到不到6秒。当然,这可以进一步优化,因为权重在计算过程中根本不会改变。

将此与一个名为生成器表达式(分别为yield关键字)的Python特性相结合,您可以构建以下内容

代码语言:javascript
复制
def throw_loaded_die(raw_mass_dist=(11, 17, 39, 44, 21, 11)):
    """return one random value from [1,6] following a probability density"""
    throws = []
    mass_dist_sum = sum(raw_mass_dist)
    mass_dist = [float(i) / mass_dist_sum for i in raw_mass_dist]
    while True:
        if not throws:
            throws = random.choices((1, 2, 3, 4, 5, 6), mass_dist, k=1000)
        yield throws.pop()

loaded_throws = throw_loaded_die()

它可以像sum(next(loaded_throws) for _ in range(8))一样在throw_dice中使用。正如@Georgy在评论中指出的那样,random.choicesraw_mass_dist中也很好,因此不需要对非NumPy版本进行严格的规范化。有关进一步的解释,请参阅这个优秀的堆叠溢出柱

我还创建了一个使用NumPy和索引的版本--非常像您最初的解决方案--以查看性能是否可以进一步提高。

代码语言:javascript
复制
    """return one random value from [1,6] following a probability density"""
    throws = []
    mass_dist = numpy.array(raw_mass_dist) / numpy.sum(raw_mass_dist)
    idx = 1000
    while True:
        if idx >= 1000:
            idx = 0
            throws = numpy.random.choice((1, 2, 3, 4, 5, 6), p=mass_dist, size=(1000, ))
        yield throws[idx]
        idx += 1

当您在脚本中使用时,此实现与我建议的第一个实现相同,不会有任何进一步的更改。一些更广泛的时间表明,NumPy/索引版本将获胜,如果您显著增加抛出的次数。

票数 4
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/221972

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档