pygame+numpy实现康威生命游戏

康威生命游戏介绍

康威生命游戏(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

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

运行展示

初始化的随机状态:

image.png

演进一段时间的状态: image.png

达到稳态: image.png


关于Python学习指南

学好 Python 不论是就业还是做副业赚钱都不错,但要学会 Python 还是要有一个学习规划。最后给大家分享一份全套的 Python 学习资料,给那些想学习 Python 的小伙伴们一点帮助!

包括:Python激活码+安装包、Python web开发,Python爬虫,Python数据分析,人工智能、自动化办公等学习教程。带你从零基础系统性的学好Python!

👉Python所有方向的学习路线👈

Python所有方向路线就是把Python常用的技术点做整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。(全套教程文末领取)

在这里插入图片描述

👉Python学习视频600合集👈

观看零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。

在这里插入图片描述

温馨提示:篇幅有限,已打包文件夹,获取方式在:文末

👉Python70个实战练手案例&源码👈

光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

在这里插入图片描述

👉Python大厂面试资料👈

我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

在这里插入图片描述

在这里插入图片描述

👉Python副业兼职路线&方法👈

学好 Python 不论是就业还是做副业赚钱都不错,但要学会兼职接单还是要有一个学习规划。

在这里插入图片描述

👉 这份完整版的Python全套学习资料已经上传,朋友们如果需要可以扫描下方CSDN官方认证二维码免费领取保证100%免费

  • 19
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值