俄罗斯方块随机生成器:random.choice与排列对比random.shuffle

时间:2013-11-02 21:16:25

标签: python algorithm random

在俄罗斯方块游戏的一些实现中,有一种称为Random Generator的算法,它基于以下算法生成一组单侧四联骨牌的无限序列排列:

  

随机生成器生成所有七个单面的序列   tetrominoes(I,J,L,O,S,T,Z)随机排列,就好像它们一样   从袋子里抽出来的。然后它将所有七个tetromino交易到这件作品   生成另一个包之前的顺序。

此无限序列的元素仅在必要时生成。即当需要比队列更多的片段时,将7个单侧四联骨牌的随机排列附加到四联骨牌队列中。

我相信在Python中有两种主要的方法。

第一种方法使用itertools.permutationsrandom.choice

import itertools, random, collections
bag = "IJLOSTZ"
bigbag = list(itertools.permutations(bag))
sequence = collections.deque(random.choice(bigbag))
sequence.extend(random.choice(bigbag))
sequence.extend(random.choice(bigbag))
# . . . Extend as necessary

第二种方法仅使用random.shuffle

import random, collections
bag = ['I', 'J', 'L', 'O', 'S', 'T', 'Z']
random.shuffle(bag)
sequence = collections.deque(bag)
random.shuffle(bag)
sequence.extend(bag)
random.shuffle(bag)
sequence.extend(bag)
# . . . Extend as necessary

两种方法的优点/缺点是什么,假设俄罗斯方块的玩家技术熟练且随机生成器必须产生大量的单侧四联体?

2 个答案:

答案 0 :(得分:2)

我会说,将一个小清单洗牌的时间简直是微不足道的,所以不要担心。任何一种方法都应该“同等随机”,因此决定那里没有依据。

但是我不是用列表和deques捣碎,而是使用tile生成器:

def get_tile():
    from random import shuffle
    tiles = list("IJLOSTZ")
    while True:
        shuffle(tiles)
        for tile in tiles:
            yield tile

简短,甜美,明显。

制作可窥探的

因为我老了,当我听到“可以排队的队列”时,我认为是“循环缓冲区”。为固定大小的缓冲区分配一次内存,并使用包裹的索引变量跟踪“下一个项目”。当然,这在C中的回报远远超过Python,但具体来说:

class PeekableQueue:
    def __init__(self, item_getter, maxpeek=50):
        self.getter = item_getter
        self.maxpeek = maxpeek
        self.b = [next(item_getter) for _ in range(maxpeek)]
        self.i = 0

    def pop(self):
        result = self.b[self.i]
        self.b[self.i] = next(self.getter)
        self.i += 1
        if self.i >= self.maxpeek:
            self.i = 0
        return result

    def peek(self, n):
        if not 0 <= n <= self.maxpeek:
            raise ValueError("bad peek argument %r" % n)
        nthruend = self.maxpeek - self.i
        if n <= nthruend:
            result = self.b[self.i : self.i + n]
        else:
            result = self.b[self.i:] + self.b[:n - nthruend]
        return result

q = PeekableQueue(get_tile())

因此,您可以通过q.pop()使用下一个图块,然后您可以随时获取通过{{1}弹出的下一个n图块列表}。并且宇宙中没有有机俄罗斯方块玩家足够快,因为这段代码的速度完全不同; - )

答案 1 :(得分:1)

有7个不同对象序列的7! = 5040个排列。因此,就时间复杂度(O(n!* n))和空间复杂度(O(n!* n))而言,生成所有排列是非常昂贵的。然而,从排列序列中选择随机排列很容易。我们来看看random.pychoice的代码。

def choice(self, seq):
    """Choose a random element from a non-empty sequence."""
    return seq[int(self.random() * len(seq))]  # raises IndexError if seq is empty

正如您所看到的,索引的计算是O(1),因为len(seq)对于任何序列都是O(1)而self.random()也是O(1)。从Python中的列表类型中获取元素也是O(1),因此整个函数是O(1)。


另一方面,使用random.shuffle会将包中的元素交换到位。因此,它将使用O(1)空间复杂度。然而,就时间复杂性而言,它并不那么有效。我们来看看random.py

shuffle的代码
def shuffle(self, x, random=None, int=int):
    """x, random=random.random -> shuffle list x in place; return None.

    Optional arg random is a 0-argument function returning a random
    float in [0.0, 1.0); by default, the standard random.random.
    """

    if random is None:
        random = self.random
    for i in reversed(xrange(1, len(x))):
        # pick an element in x[:i+1] with which to exchange x[i]
        j = int(random() * (i+1))
        x[i], x[j] = x[j], x[i]

random.shuffle实现Fisher-Yates shuffle,“类似于从帽子中随机挑选编号的门票,或从甲板上随意挑选卡片,一个接一个,直到不再剩下。”但是,由于必须对len(x)-1进行random()调用,并且还需要len(x)-1交换操作,因此计算次数明显大于第一种方法。每个交换操作都需要从列表中获取2次,并生成用于解包和赋值的2元组。


根据所有这些信息,我猜测所提到的第一种方法占用大量内存来存储排列,并且需要O(n!* n)时间复杂度开销,但从长远来看,它可能是比第二种方法更有效,并且可能在实际的俄罗斯方块游戏实施中保持帧率稳定,因为在实际游戏循环期间将进行较少的计算。可以在显示甚至初始化之前生成排列,这很好地给出了游戏不执行许多计算的错觉。


在这里,我使用Tim Peters建议的生成器和循环缓冲区发布最终代码。由于循环缓冲区的大小在创建缓冲区之前是已知的,并且它永远不会改变,因此我没有实现循环缓冲区通常具有的所有功能(您可以在Wikipedia article上找到它)。在任何情况下,它都适用于随机生成器算法。

def random_generator():
    import itertools, random
    bag = "IJLOSTZ"
    bigbag = list(itertools.permutations(bag))
    while True:
        for ost in random.choice(bigbag):
            yield ost


def popleft_append(buff, start_idx, it):
    """ This function emulates popleft and append from
        collections.deque all in one step for a circular buffer
        of size n which is always full,
        The argument to parameter "it" must be an infinite 
        generator iterable, otherwise it.next() may throw
        an exception at some point """
    left_popped = buff[start_idx]
    buff[start_idx] = it.next()
    return (start_idx + 1) % len(buff), left_popped


def circular_peek(seq, start_idx):
    return seq[start_idx:len(seq)] + seq[:start_idx]


# Example usage for peek queue of size 5
rg = random_generator()
pqsize = 5
# Initialize buffer using pqsize elements from generator
buff = [rg.next() for _ in xrange(pqsize)]
start_idx = 0
# Game loop
while True:
    # Popping one OST (one-sided tetromino) from queue and 
    # adding a new one, also updating start_idx
    start_idx, left_popped = popleft_append(buff, start_idx, rg)
    # To show the peek queue currently
    print circular_peek(buff, start_idx)
相关问题