康威生命游戏介绍
此处直接照搬另一博主的博客了:blog.csdn.net/cnds123/art...
康威生命游戏(Conway's Game of Life)是一种零玩家游戏,属于元胞自动机的一种。它由数学家约翰·康威(John Horton Conway)在1970年发明。生命游戏在一个无限的二维网格上进行,每个格子代表一个细胞,每个细胞有两种状态:存活 或死亡。(just like humans)
细胞的下一个状态由其周围的八个邻居细胞的状态按以下规则决定(康威生命游戏的规则是):
如果一个存活的细胞周围有2个或3个存活的邻居,那么该细胞在下一代中继续存活。 如果一个死亡的细胞周围正好有3个存活的邻居,那么该细胞在下一代中变为存活状态。 在所有其他情况下,一个细胞要么保持死亡状态,要么变为死亡状态。
随着游戏的进行,细胞群体将经历多代的演化,可能会出现以下几种情况: 稳定状态:细胞群体停止了变化,达到了稳定状态。也就是说,所有的细胞都遵循规则1和规则2,没有细胞死亡或新生。 摆动(振荡)状态:细胞群体在两种或多种状态之间循环变化。也就是说,每过几代,细胞群体会回到原来的状态。 移动状态:细胞群体作为一个整体在网格中移动。也就是说,每过一代,细胞群体的位置会改变,但其形状保持不变。 灭绝状态,因为所有的细胞都死亡了。在这个状态下,如果没有新的细胞被添加到游戏中,网格将一直保持空白,因为已经没有细胞可以复活或新生了。 混乱状态:细胞群体在持续变化,并没有显示出稳定、摆动或移动的模式。
需要注意的是,康威生命游戏是一个确定性的游戏,也就是说,给定一个初始状态,下一代的状态是确定的,不依赖于随机因素。换句话说,如果你从同一个初始配置开始,每次运行游戏都会得到相同的结果。因此,以上所描述的所有可能的情况都是可以预测的。尽管游戏是确定性的,但由于某些模式可能非常复杂,预测它们的长期行为可能在实践中是非常困难的,尤其是对于大型和复杂的初始配置。
环境依赖:
shell
python 3.6+
pygame
numpy
实现
主程序
此处为主要pygame的界面展示内容,还是照搬了上面那位博主的代码,封装了一下
python
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
的格子进行计算。
python
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类主要是用于计算网格位置的。
python
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
核心代码就是上述内容了。
运行展示
初始化的随机状态:
演进一段时间的状态:
达到稳态: