likes
comments
collection
share

pygame+numpy实现康威生命游戏

作者站长头像
站长
· 阅读数 4

康威生命游戏介绍

此处直接照搬另一博主的博客了:blog.csdn.net/cnds123/art…

康威生命游戏(Conway's Game of Life)是一种零玩家游戏,属于元胞自动机的一种。它由数学家约翰·康威(John Horton Conway)在1970年发明。生命游戏在一个无限的二维网格上进行,每个格子代表一个细胞,每个细胞有两种状态:存活死亡。(just like humans)

细胞的下一个状态由其周围的八个邻居细胞的状态按以下规则决定(康威生命游戏的规则是):

如果一个存活的细胞周围有2个或3个存活的邻居,那么该细胞在下一代中继续存活。 如果一个死亡的细胞周围正好有3个存活的邻居,那么该细胞在下一代中变为存活状态。 在所有其他情况下,一个细胞要么保持死亡状态,要么变为死亡状态。

随着游戏的进行,细胞群体将经历多代的演化,可能会出现以下几种情况: 稳定状态:细胞群体停止了变化,达到了稳定状态。也就是说,所有的细胞都遵循规则1和规则2,没有细胞死亡或新生。 摆动(振荡)状态:细胞群体在两种或多种状态之间循环变化。也就是说,每过几代,细胞群体会回到原来的状态。 移动状态:细胞群体作为一个整体在网格中移动。也就是说,每过一代,细胞群体的位置会改变,但其形状保持不变。 灭绝状态,因为所有的细胞都死亡了。在这个状态下,如果没有新的细胞被添加到游戏中,网格将一直保持空白,因为已经没有细胞可以复活或新生了。 混乱状态:细胞群体在持续变化,并没有显示出稳定、摆动或移动的模式。

需要注意的是,康威生命游戏是一个确定性的游戏,也就是说,给定一个初始状态,下一代的状态是确定的,不依赖于随机因素。换句话说,如果你从同一个初始配置开始,每次运行游戏都会得到相同的结果。因此,以上所描述的所有可能的情况都是可以预测的。尽管游戏是确定性的,但由于某些模式可能非常复杂,预测它们的长期行为可能在实践中是非常困难的,尤其是对于大型和复杂的初始配置。

环境依赖:

python 3.6+
pygame
numpy

实现

主程序

此处为主要pygame的界面展示内容,还是照搬了上面那位博主的代码,封装了一下

import os
from common.liveZone import LiveZone
from common.config import Config
from common.argsParser import ArgsParser
import sys
from typing import Any, Dict, List, Union
from tkinter import Tk, messagebox
import threading
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"  # 隐藏pygame导入时的欢迎信息

import pygame


class Window(object):
    # 设置颜色
    WHITE = (255, 255, 255)
    BLACK = (0, 0, 0)
    GRAY = (200, 200, 200)

    def __init__(self, input_args: Any) -> None:
        self.config = self.__parser_all_config(input_args)

        self.screen = None
        self.cell_size = None
        self.init_window()
        self.live_zone = self.init_cell_array()
        self.dialog_open = False  # 存储对话框状态的变量

    @staticmethod
    def __parser_all_config(args: List[Any]):
        # 解析项目启动传参
        args_parser = ArgsParser(__name__)
        args_parser.parse_args(args=args)
        config = Config()
        for key, value in args_parser.arg_items.items():
            config.set_config(key=key, value=value)
        return config

    def __get_config_value(self, key: str) -> Any:
        """获取配置文件的参数"""
        value = self.config.get_value(key)
        if isinstance(value, str):
            if value == "None":
                value = None
        return value

    def init_window(self) -> None:
        # 初始化Pygame
        pygame.init()
        width = self.__get_config_value("width")
        height = self.__get_config_value("height")
        rows = self.__get_config_value("rows")
        cols = self.__get_config_value("cols")

        # 设置窗口大小
        self.screen = pygame.display.set_mode((width, height))
        pygame.display.set_caption("康威生命游戏")

        # 设置网格大小
        self.cell_size = width // cols

    def init_cell_array(self) -> LiveZone:
        mode = self.__get_config_value("mode")
        data = self.__get_config_value("data")
        rows = self.__get_config_value("rows")
        cols = self.__get_config_value("cols")
        return LiveZone(rows=rows, cols=cols, mode=mode, data=data)

    def game_start(self) -> None:
        rows = self.__get_config_value("rows")
        cols = self.__get_config_value("cols")
        # 游戏循环
        running = True
        pause = True  # 开始时暂停游戏
        while running:
            self.screen.fill(self.WHITE)
            # 处理事件
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                if event.type == pygame.MOUSEBUTTONDOWN:
                    col, row = pygame.mouse.get_pos()
                    col = col // self.cell_size
                    row = row // self.cell_size
                    if event.button == 1:  # 左键
                        self.live_zone.lz_arr[row, col] = 1
                    elif event.button == 3:  # 右键
                        self.live_zone.lz_arr[row, col] = 0
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        pause = not pause  # 按空格键暂停或开始游戏
                    if event.key == pygame.K_F1:  # 按下F1键,弹出提示对话框
                        if not self.dialog_open:
                            threading.Thread(target=self.show_help_dialog).start()
            # 绘制网格线
            for row in range(rows):
                for col in range(cols):
                    rect = pygame.Rect(col * self.cell_size, row * self.cell_size, self.cell_size, self.cell_size)
                    if self.live_zone.lz_arr[row, col] == 1:
                        pygame.draw.rect(self.screen, self.BLACK, rect)
                    pygame.draw.rect(self.screen, self.GRAY, rect, 1)  # 绘制网格线

            # 更新网络
            if not pause:
                self.live_zone.life_propagates()
                result = self.live_zone.lz_arr

            pygame.display.flip()
            pygame.time.delay(100)

    @staticmethod
    def quit():
        pygame.quit()

    def show_help_dialog(self):
        self.dialog_open = True
        root = Tk()
        root.withdraw()  # 隐藏主窗口
        messagebox.showinfo("游戏帮助: ",
                            "- 单击鼠标左键放置细胞\n"
                            "- 单击鼠标右键移除细胞\n"
                            "- 按空格键开始/暂停游戏\n"
                            "- 按F1键显示此帮助")
        root.destroy()
        self.dialog_open = False


if __name__ == '__main__':
    window = Window(input_args=sys.argv)
    try:
        window.game_start()
    except Exception as e:
        window.quit()

细胞类

封装了单个细胞类,包含生命游戏的规则处理;细胞网格位置类主要是为了计算出当前细胞周围的位置,按照3*3的格子进行计算。

from typing import Tuple, Any, Iterator
from enum import IntEnum


class CellGridPosition(object):
    _instance = None

    @classmethod
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super(CellGridPosition, cls).__new__(*args, **kwargs)
        return cls._instance

    def __init__(self) -> None:
        self.up_left = None
        self.up = None
        self.up_right = None
        self.left = None
        self.centre = None
        self.right = None
        self.down_left = None
        self.down = None
        self.down_right = None

    def set_attr(self, key: str, value: Any) -> None:
        setattr(self, key, value)

    def clear(self) -> None:
        self.up_left = None
        self.up = None
        self.up_right = None
        self.left = None
        self.centre = None
        self.right = None
        self.down_left = None
        self.down = None
        self.down_right = None

    def get_cells(self) -> Iterator:
        """
        获取计算后的所有细胞位置
        :return: 细胞索引位置的可迭代对象
        """
        for attr_name, value in self.__dict__.items():
            if attr_name.startswith("_"):
                # 跳过隐藏的属性
                continue
            yield value

    def get_round_cells(self) -> Iterator:
        """
        获取中心细胞周围的细胞的位置
        :return: 细胞索引位置的可迭代对象
        """
        for attr_name, value in self.__dict__.items():
            if (attr_name.startswith("_")) or (attr_name == "centre"):
                # 跳过隐藏的属性以及中心位置
                continue
            yield value


class CellStatus(IntEnum):
    live: int = 1
    died: int = 0


class Cell(object):
    def __init__(self, pos: Tuple[int, int], status: IntEnum=None) -> None:
        self.x, self.y = pos
        self.status = self.__init_cell_status(status=status)

    @staticmethod
    def __init_cell_status(status: IntEnum) -> int:
        if status is None:
            return CellStatus.died
        else:
            if status == 0:
                return CellStatus.died
            return CellStatus.live

    def update_status(self) -> None:
        if self.status == CellStatus.live:
            self.status = CellStatus.died
        elif self.status == CellStatus.died:
            self.status = CellStatus.live
        else:
            raise RuntimeError(f"cell status attr must in 0 or 255, 0 means died 255 means alive.")

    def turn_live(self) -> None:
        if self.status == CellStatus.died:
            self.status = CellStatus.live

    def turn_died(self) -> None:
        if self.status == CellStatus.live:
            self.status = CellStatus.died

    def get_status(self) -> int:
        return self.status

    def is_alive(self) -> bool:
        return self.status == CellStatus.live

    def is_dead(self) -> bool:
        return self.status == CellStatus.died

    def evolution_of_live(self, round_cnt: int) -> None:
        """生命演化方法,根据康威生命游戏规则判断当前细胞的存活状态
        live game rule:
        1.当周围仅有1个或没有存活细胞时, 原来的存活细胞进入死亡状态。(模拟生命数量稀少)
        2.当周围有2个或3个存活细胞时, 网格保持原样。
        3.当周围有4个及以上存活细胞时,原来的存活细胞亦进入死亡状态。(模拟生命数量过多)
        4.当周围有3个存活细胞时,空白网格变成存活细胞。(模拟繁殖)
        """
        if self.is_alive():
            if (0 <= round_cnt <= 1) or (round_cnt >= 4):
                # 1.当周围仅有1个或没有存活细胞时, 原来的存活细胞进入死亡状态。(模拟生命数量稀少)
                # 3.当周围有4个及以上存活细胞时,原来的存活细胞亦进入死亡状态。(模拟生命数量过多)
                self.turn_died()
            elif 2 <= round_cnt <= 3:
                # 2.当周围有2个或3个存活细胞时, 网格保持原样。
                pass  # keep going
        elif self.is_dead():
            if round_cnt == 3:
                # 4.当周围有3个存活细胞时,空白网格变成存活细胞。(模拟繁殖)
                self.turn_live()

康威生命区域类

LiveZone类主要是用于生成区域内的细胞,可以随机或者是用户自定义(目前版本用户传参需要传入完整的区域状态字符串,例如3*3的需要传入0,0,0,1,0,1,0,1,0,范围大的话传参非常麻烦,后续考虑一下改进这块内容)。该类封装了生命游戏演进的方法,直接进行康威生命游戏的演化。GridCalculator类主要是用于计算网格位置的。

import numpy as np
from typing import Tuple, Union, Any, Dict, List, Iterator
from common.Cell import CellStatus, Cell, CellGridPosition


class GridCalculator(object):
    """
    网格细胞周围位置计算类
    """
    POSITIONS = [
        "up_left",  # 左上
        "up",  # 上
        "up_right",  # 右上
        "left",  # 左
        "centre",  # 中心
        "right",  # 右
        "down_left",  # 左下
        "down",  # 下
        "down_right"  # 右下
    ]

    def __init__(self):pass

    @staticmethod
    def cal_neighbor(cell_grid_position: CellGridPosition, row_index:  int, col_index: int) -> None:
        """
        中心细胞周围位置计算方法
        :param cell_grid_position: 细胞网格位置类
        :param row_index: 处于中心位置的细胞位置行索引
        :param col_index: 处于中心位置的细胞位置列索引
        :return:
        """
        cell_grid_position.clear()  # 重置位置信息
        cell_grid_position.set_attr("centre", (row_index, col_index))
        cell_grid_position.set_attr("up_left", (row_index - 1, col_index - 1))
        cell_grid_position.set_attr("up", (row_index - 1, col_index))
        cell_grid_position.set_attr("up_right", (row_index - 1, col_index + 1))
        cell_grid_position.set_attr("left", (row_index, col_index - 1))
        cell_grid_position.set_attr("right", (row_index, col_index + 1))
        cell_grid_position.set_attr("down_left", (row_index + 1, col_index - 1))
        cell_grid_position.set_attr("down", (row_index + 1, col_index))
        cell_grid_position.set_attr("down_right", (row_index + 1, col_index + 1))


class LiveZone(object):
    def __init__(self, rows: int, cols: int, mode: str="random", *args, **kwargs) -> None:

        self.mode: str = mode
        self.__lz_arr: np.array = self.__init_zone_arr(rows, cols, *args, **kwargs)
        self.rows, self.cols = self.__get_shape()
        self.grid_calculator: GridCalculator = GridCalculator()
        self.__cell_grid_position: CellGridPosition = CellGridPosition()

    @staticmethod
    def __init_user_defined_arr(data: str) -> np.array:
        values = data.split(",")
        value_binary = []
        for value in values:
            value_binary.append(int(value) % 2)
        return np.asarray(value_binary)

    def __init_zone_arr(self, rows: int, cols: int, *args, **kwargs) -> np.array:
        if self.mode == "random":
            arr = np.random.choice([0, 1], size=rows*cols)
        elif self.mode.lower() == "userdefined":
            data = kwargs.get("data", None)
            if data is None:
                return None
            else:
                arr = self.__init_user_defined_arr(data)
        else:
            return None
        return arr.reshape(rows, cols)

    def __get_shape(self) -> Tuple[int, int]:
        if self.__lz_arr is None:
            return 0, 0
        else:
            return self.__lz_arr.shape

    @property
    def lz_arr(self) -> np.array:
        return self.__lz_arr

    @lz_arr.setter
    def lz_arr(self, arr: np.array) -> None:
        self.__lz_arr = arr

    @staticmethod
    def get_sub_arr_rows(row_shape: int, i: int) -> Tuple[int, int]:
        start_row = max(0, i - 1)
        end_row = min(row_shape, i + 2)
        return start_row, end_row

    @staticmethod
    def get_sub_arr_cols(col_shape: int, j: int) -> Tuple[int, int]:
        start_col = max(0, j - 1)
        end_col = min(col_shape, j + 2)
        return start_col, end_col

    def extract_subarray(self, arr, i, j):
        row_shape, col_shape = arr.shape
        if (row_shape < 3) or (col_shape < 3):
            raise ValueError("The input array must be at least 3x3")

        start_row, end_row = self.get_sub_arr_rows(row_shape=row_shape, i=i)
        start_col, end_col = self.get_sub_arr_cols(col_shape=col_shape, j=j)
        return arr[start_row:end_row, start_col:end_col]

    def check_bounds(self, row: Union[int, None], col: Union[int, None]) -> bool:
        """
        索引越界检测
        :param row:
        :param col:
        :return:
        """
        if (row is not None) and (col is not None):
            if row < 0 or row >= self.rows:
                row_check = False
            else:
                row_check = True
            if col < 0 or col >= self.cols:
                col_check = False
            else:
                col_check = True
            return row_check and col_check
        elif (row is None) or (col is None):
            return False
        else:
            raise ValueError("At least one of the arguments must be provided")

    def sub_zone_life_propagates(self) -> int:
        live_round_cnt = 0
        for cell_position in self.__cell_grid_position.get_round_cells():
            row, col = cell_position
            if self.check_bounds(row, col):
                # 不越界的情况
                status = self.__lz_arr[row, col]
                if status == 1:
                    live_round_cnt += 1
            else:
                # 越界
                continue
        return live_round_cnt

    def life_propagates(self) -> None:
        """ live game rule
        1.当周围仅有1个或没有存活细胞时, 原来的存活细胞进入死亡状态。(模拟生命数量稀少)
        2.当周围有2个或3个存活细胞时, 网格保持原样。
        3.当周围有4个及以上存活细胞时,原来的存活细胞亦进入死亡状态。(模拟生命数量过多)
        4.当周围有3个存活细胞时,空白网格变成存活细胞。(模拟繁殖)
        """
        new_live_zone = np.zeros(shape=(self.rows, self.cols), dtype=int)

        for row_index in range(self.rows):
            for col_index in range(self.cols):
                # 遍历生命区域中的每一个位置,判断该位置的细胞状态
                self.grid_calculator.cal_neighbor(self.__cell_grid_position, row_index, col_index)
                live_round_cnt = self.sub_zone_life_propagates()
                cell = Cell(pos=(row_index, col_index), status=self.__lz_arr[row_index, col_index])
                cell.evolution_of_live(live_round_cnt)
                new_live_zone[row_index, col_index] = cell.get_status()

        self.__lz_arr = new_live_zone

核心代码就是上述内容了。

运行展示

初始化的随机状态:

pygame+numpy实现康威生命游戏

演进一段时间的状态: pygame+numpy实现康威生命游戏

达到稳态: pygame+numpy实现康威生命游戏

gitee

gitee.com/saberbin/li…