首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >用Python函数改进Conway的生命游戏代码

用Python函数改进Conway的生命游戏代码
EN

Code Review用户
提问于 2021-11-28 03:59:06
回答 2查看 862关注 0票数 2

这个问题是康威生命游戏的一部分。目前,这个程序需要大约70行Python代码才能返回游戏的功能,这可以简化为较少的代码行,并在键盘中断发生时结束。我最终想要消除键盘中断,并改进这段代码,也许尝试和除了功能,让游戏显示一条消息,说明它已经结束。有什么建议可以用一些额外的功能来清理这个代码吗?

就像任何游戏一样,规则是有的!下面是一个简单的概述。这个游戏的核心是四条规则,它们决定细胞是活的还是死的。这完全取决于那个细胞的邻居中有多少人还活着。

  1. 出生:相邻三个活的邻居的每一个死细胞都将在下一代中存活。
  2. 与世隔绝的死亡:每一个活的细胞与一个或更少的活邻居将在下一代死亡。
  3. 过度拥挤导致的死亡:每一个有四个或更多活邻居的活细胞都会在下一代中死亡。
  4. 生存:每一个活的细胞,无论是两个或三个活的邻居,将保持活着的下一代。

规则同时适用于所有单元格。

代码语言:javascript
复制
# Conway's Game of Life
import random
import time
import copy

WIDTH = 60
HEIGHT = 20

# Create a list of list for the cells:
nextCells = []
for x in range(WIDTH):
    column = [] # Create a new column.
    for y in range(HEIGHT):
        if random.randint(0, 1) == 0:
            column.append('#') # Add a living cell.
    else:
        column.append(' ') # Add a dead cell.
    nextCells.append(column) # nextCells is a list of column lists.

while True: # Main program loop.
    print('\n\n\n\n\n') # Separate each step with newlines.
    currentCells = copy.deepcopy(nextCells) 

# Print currentCells on the screen:
for y in range(HEIGHT):
    for x in range(WIDTH):
        print(currentCells[x][y], end='') # Print the # or space.
    print() # Print a newline at the end of the row.

# Calculate the next step's cells based on current step's cells:
for x in range(WIDTH):
    for y in range(HEIGHT):
        # Get neighboring coordinates:
        # `% WIDTH` ensures leftCoord is always between 0 and WIDTH - 1
        leftCoord  = (x - 1) % WIDTH
        rightCoord = (x + 1) % WIDTH
        aboveCoord = (y - 1) % HEIGHT
        belowCoord = (y + 1) % HEIGHT

        # Count number of living neighbors:
        numNeighbors = 0
        if currentCells[leftCoord][aboveCoord] == '#':
            numNeighbors += 1 # Top-left neighbor is alive.
        if currentCells[x][aboveCoord] == '#':
            numNeighbors += 1 # Top neighbor is alive.
        if currentCells[rightCoord][aboveCoord] == '#':
            numNeighbors += 1 # Top-right neighbor is alive.
        if currentCells[leftCoord][y] == '#':
            numNeighbors += 1 # Left neighbor is alive.
        if currentCells[rightCoord][y] == '#':
            numNeighbors += 1 # Right neighbor is alive.
        if currentCells[leftCoord][belowCoord] == '#':
            numNeighbors += 1 # Bottom-left neighbor is alive.
        if currentCells[x][belowCoord] == '#':
            numNeighbors += 1 # Bottom neighbor is alive.
        if currentCells[rightCoord][belowCoord] == '#':
            numNeighbors += 1 # Bottom-right neighbor is alive.

        # Set cell based on Conway's Game of Life rules:
        if currentCells[x][y] == '#' and (numNeighbors == 2 or
numNeighbors == 3):  
            # Living cells with 2 or 3 neighbors stay alive:
            nextCells[x][y] = '#'
        elif currentCells[x][y] == ' ' and numNeighbors == 3:
            # Dead cells with 3 neighbors become alive:
            nextCells[x][y] = '#'
        else:
            # Everything else dies or stays dead:
            nextCells[x][y] = ' '
time.sleep(1) # Add a 1-second pause to reduce flickering.
EN

回答 2

Code Review用户

发布于 2021-11-30 11:40:25

您的代码工作和可读性,这是很好的。它也大多遵循PEP8风格的约定,这很好,尽管这一点可以改进,因为在另一个答案中已经提到了它。

当然,总有改进的余地。

码结构

您的代码所缺少的是结构:一切都发生在顶层,而不使用函数或类。

这是不好的,因为它使代码更难阅读和遵循,更少的可维护性和可重用性。

将代码分解为函数可以让您独立地关注代码的每个方面。考虑以下伪码:

代码语言:javascript
复制
initialize()

while True:
    display_cells()
    update_state()
    wait()

在这种情况下,主循环非常简单,易于跟踪。现在,如果您想要研究如何显示单元格,您可以轻松地转到相关函数的定义上,并在此基础上进行工作。如果您想尝试另一种显示方式,例如使用图形显示而不是控制台上的字符,则可以定义另一个函数并替换主循环中的一行来尝试。

它还表明,它需要一种方法来跟踪单元格网的当前状态。

在代码中,使用一个名为nextCells的全局变量。全局变量被认为是错误的做法有很多很好的原因,主要是因为它使得很难跟踪它们所执行的代码中的什么地方,因此它的值应该在代码中的给定位置,特别是当您将代码分解成多个函数时。

修复该问题的一种方法是将变量作为参数传递给函数:

代码语言:javascript
复制
cells = initialize()
while True:
    display_cells(cells)
    cells = update_state(cells)
    wait()

您可以认为,cells在技术上仍然是一个全局变量,因为它是在代码的根级定义的。处理这个问题的一种方法是将游戏状态封装到一个类中。该类应保持游戏状态,提供对其采取行动的方式,并访问有关它的信息。

实现类时,代码现在如下所示:

代码语言:javascript
复制
class GameOfLife:
    def __init__(self):
        pass
    def update_state(self):
        pass
    def print_cells(self)
        pass
    def run(self)
        pass


game = GameOfLife()
game.run()

现在只剩下几件事要解决了。

首先是文档:虽然代码很简单,而且大多是自文档化,但如果有必要,最好使用docstring和注释来记录代码。

Docstring应该描述类和函数的用途,以及如何使用它们。如果类/函数在其他地方使用,则可以使用Python的help()内置函数访问它们,并允许用户阅读如何使用您的工作而无需阅读实际代码。

如果还不清楚,评论应该提供为什么事情是以某种方式进行的。如果代码是使用描述性名称和逻辑单元编写的,则不需要注释。

最后是可重用性。既然生命游戏已经封装到一个方便的对象中,那么从另一个脚本中import并使用它是非常有用的。然而,实际上,最后2行将执行,游戏的一个实例将运行,在这种情况下这是不可取的。

为了允许imports并运行脚本,您可以将代码放入“主保护程序”中。

我对这个问题的看法是:

代码语言:javascript
复制
import random
from time import sleep


class Life:
    '''
    An implementation of Conway's game of life on a finite and wrapping grid
    '''
    
    LIVE_CELL = '#'
    DEAD_CELL = ' '
    DELAY = 1

    def __init__(self, width=60, height=20):
        self.width = width
        self.height = height
        self.cells = [[random.choice([True, False]) for _ in range(self.width)] for _ in range(self.height)]

    def next_generation(self):
        '''
        Updates the cell grid according to Conway's rules
        '''
        next_cells = [[False for _ in range(self.width)] for _ in range(self.height)]
        for j in range(self.width):
            for i in range(self.height):
                neighbor_count = self._count_live_neighbors(i, j)
                if ((self.cells[i][j] and neighbor_count in (2, 3))
                    or (not self.cells[i][j] and neighbor_count == 3)):
                    next_cells[i][j] = True
        self.cells = next_cells

    def _count_live_neighbors(self, i, j):
        '''
        Counts the number of live neighbor of a cell, given the cell indices on the grid
        '''
        count = 0
        for di, dj in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]:
            if self.cells[(i + di) % self.height][(j + dj) % self.width]:
                count += 1
        return count

    def print_cells(self):
        '''
        Print the current state of the cell grid on the console
        '''
        print('\n\n\n\n\n')
        for row in self.cells:
            chars = [self.LIVE_CELL if c else self.DEAD_CELL for c in row]
            print(''.join(chars))

    def run(self):
        '''
        Run the game of life until a keyboard interrupt is raise (by pressing ctrl+c)
        '''
        try:
            while(True):
                self.print_cells()
                self.next_generation()
                sleep(self.DELAY)
        except KeyboardInterrupt:
            pass


if __name__ == '__main__':
    life = Life()
    life.run()

其他改进

正如你所看到的,我也改变了一些逻辑,所以我将解释我的推理。

使用用于单元格

的布尔值数组

True代表活细胞,False代表死细胞。这是为了提高可重用性,让显示逻辑决定如何显示死活的单元格。它还简化了一些逻辑。

使用循环遍历邻居

较少的复制和粘贴代码使其更容易出错和更易于维护,但在这种情况下仍然很容易理解。

使用列表理解

Python一行有时很难读,但是列表理解来初始化列表是非常有用的,而且比迭代附加值更有效。

在这种简单的情况下,它仍然是相当可读的。

更进一步

如果您对进一步的练习感兴趣,我可以考虑对代码进行一些改进:

  • 实现其他方法来初始化游戏,例如将一个单元格传递给构造函数(容易),或者解析一个文件(稍微困难一点)。这样就可以尝试一些模式。
  • 正如John所描述的,生命的典型博弈是在无限网格上进行的。虽然这在实践中是不可能的,但可以通过在实际显示的(媒体)之外的一段距离内跟踪游戏状态来进行近似。
  • 在控制台上显示不是很好,因为显示的单元格看起来不太好,不是方形的,而且数量非常有限,而控制台则闪烁。图形显示将更适合,但也很难实现。
  • 更新游戏状态可以进行很大的优化。对于少量的单元格(以及控制台显示)来说,这是可以的,但是如果您最终移动到一个大型网格中,这个解决方案可能还不够好。我能想到的可能的改进:
    • 到目前为止,每次更新网格状态时,都会分配一个新的列表,然后垃圾收集器会释放旧的列表。这两种行动都是缓慢的。解决方案是将2个列表保存在内存中,并指向当前代和下一代的两个列表。
    • 计算下一代的状态很容易并行化,因为每个单元是独立于其他单元的。
票数 3
EN

Code Review用户

发布于 2021-11-28 17:01:41

压痕

请修复您的代码缩进,因为它现在是痛苦的,它是痛苦的,应该发生什么。这些评论提供了一些建议,以使你也容易。

常数

活细胞和死细胞的特征应该是模块级常量。使它们易于更改,代码更易读:

代码语言:javascript
复制
LIVING_CELL = '#'
DEAD_CELL = ' '
CELLS = (LIVING_CELL, DEAD_CELL)

variable_names

PEP8声明变量和函数名应该跟在snake_case后面。

nextCells -> next_cells

列表创建

清单理解常常比手动列表理解更好。考虑一下改进后的next_cells创建:

代码语言:javascript
复制
import random

next_cells = []

for _ in range(WIDTH):
    column = [random.choice(CELLS) for _ in range(HEIGHT)]
    next_cells.append(column)

您甚至可以更进一步,进行嵌套理解:

代码语言:javascript
复制
next_cells = [[random.choice(CELLS) for _ in range(HEIGHT)] for _ in range(WIDTH)]

我想说,这一行仍然是相当可读的,可能甚至比第一个建议,因此我推荐它。

random.choice是一个更好的选择,可以在这个片段中表达您的意图。

命名迭代变量_是一种表明变量值从未被实际使用过的惯例。

主逻辑

你在数邻居的时候做了太多的手工检查。不应手动构造所有可能的坐标组合。相反,考虑一下底层逻辑,以及如何将其转换为更简洁的代码。我确信也有许多资源(可能仅在这个站点上)用于正确地检查/计数给定坐标的邻居。

请注意,原来的游戏规则是为无限大小的板而设计的,据我所知,对于有限大小的板没有“官方”规则。

print('\n\n\n\n\n')更好地表示为print('\n' * 5)

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

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

复制
相关文章

相似问题

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