其实也不是一个很大的项目,但由于初学python,摸索着前进,断断续续写了三天,其中涉及到了UI、多线程、函数式编程等相对高级一点的功能,接触到了例如函数作为参数这种python奇特的特性,因此也碰到了不少麻烦,正因如此,也是收获良多。
项目简介
什么是细胞自动机
细胞自动机(cellular automata)是为模拟包括自组织结构在内的复杂现象提供的一个强有力的方法,也称为元胞自动机(Cellular Automaton)。细胞自动机模型的基本思想是:自然界里许多复杂结构和过程,归根到底只是由大量基本组成单元的简单相互作用所引起。细胞自动机主要研究由小的计算机或部件,按邻域连接方式连接成较大的、并行工作的计算机或部件的理论模型。
实际上它是探究复杂结构的的一种模型。基于最小单元的相互作用关系,最终试探出其演变结果。
细胞自动机的规则十分简单,但就是基于这种及其简单的规则,却可以演化出各种各样的图形,有一种‘一生二,二生三,三生万物’的玄学感。
细胞自动机规则
每一个小格代表一个细胞,灰色代表死亡,黑色代表存活,一个细胞周围八个细胞中有2个细胞为存活状态时,细胞保持原状,当周围为3个存活细胞时,该细胞变为或保持存活状态,其余情况均变为或保持死亡状态。
操作说明
初始状态下细胞全部为死亡状态,点击任意细胞可改变其状态。细胞设置完成后点击选项→开始,即可开始运行。使用暂停功能时,细胞自动机暂停到某一状态。点击重置可以使自动机重置为全部死亡状态。保存功能可将当前所有存活细胞的坐标保存到本地。
设置选项下,可以调节细胞自动机的运行速度以及使用预设图案和随机图案
实现原理
项目稍后会附上代码及注释,请读者自行阅读,此处主要介绍项目实现过程中碰到的几个麻烦及解决方案。
1.按钮大小的调整
这个项目原计划是使用空白按钮代表细胞,但python tkinter包下的button类有一个很不友好的特点:当按钮内容是文本时,长与宽设置的单位是字符并且只能是整数,而一个空按钮默认为内容为文本,这种情况下,很难获得一个合适大小的正方形的button。
起初以为可以通过传入参数或者给长度增加其他单位,使button的单位制定为像素,但经过尝试和百般查找,发现并行不通。
而后也尝试过给布局方法pack、grid的fill,expand等传入参数,希望通过拉伸、填充解决这个问题,发现仍然行不通。
而后,尝试以图案作为按钮的内容,然而在这种情况下背景被覆盖,因此无法通过改变背景颜色表示细胞状态。通过两张图片的不断转换有显得十分麻烦。
最终,通过系统内置位图填充,由于位图是灰色透明的,并不会遮盖背景。
2.按钮的command参数要求传入函数但并不能带参数
由于每个按钮的功能都是改变自身的颜色,通过参数指定哪一个按钮即可,然后command的参数为函数,不可以带函数。又不可能给每个按钮单独编写一个方法。
起初妄图通过直接往函数中填入参数实现,发现会直接调用。。。
此处提供两个解决方法:
1.通过 lambda方法,作为匿名函数传入:command=lambda i=i,j=j:method(i,j)
2.通过偏函数,将传入参数后的方法作为一个不需要参数的方法:functools.partial(setColor, i=i, j=j)
特别提醒,command等要求函数作为参数的情况下,参数为函数名,一定不要带括号,不然会引起调用。
3.UI界面的显示是一个死循环,因此单线程下无法执行细胞变换的运算(同时解决了暂停开始问题)
很显然,需要引入多线程,我们给计算细胞状态的方法另开辟一个线程。但这个线程一定要在主线程窗口显示方法执行之前执行,不然永远运行不到另这个线程执行的命令。(也可以给显示另外开辟一个线程)
但是,负责计算细胞状态的线程是需要在用户给出初始细胞分别状况后才开始的,这就引发矛盾。
于是我引入了线程锁,在在使计算细胞状态线程运行以前,先创建一个线程锁并由主线程申请这个锁,而计算子线程每次计算前都要先申请一下这个锁,计算完成后,释放线程锁。这样只需要通过主线程对锁的申请和释放就可以完成开始和暂停等操作。
4.通过修改time.sleep()方法的参数,改变游戏速度,但独立函数执行过程中改变已经传入其中的参数并不会对函数造成影响
函数的参数在传入后不会再受外部影响,因此,通过直接修改对应参数来调节细胞自动机速度是不可行的。
因此,将控制细胞存活或者死亡的函数放入一个类中,将控制速度的参数speed设置为类的公有属性,通过修改这个类的speed属性即可调节自动机速度。
代码详解
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'细胞自动机'
__author__ = 'Little Huang'
import random
import threading
import time
import tkinter.messagebox
import functools
import tkinter as tk # 使用Tkinter前需要先导入
def creatButtons(window, size): # 创建按钮的方法
length = 600 / size - 1
buttons = []
def setColor(i, j): # 颜色转换的功能
# print(i)
if buttons[i][j]['bg'] == 'black':
buttons[i][j]['bg'] = 'white'
else:
buttons[i][j]['bg'] = 'black'
for i in range(size):
buttons.append([]) # 使用数组管理
for j in range(size):
buttons[i].append(tk.Button(master=window, bitmap='gray12', width=length, height=length, bg='white',
command=functools.partial(setColor, i=i, j=j)))
# 此处内容填入了一个系统自带的位图,便于调节按钮的大小
# 因为command参数为函数名,无法带参数,故此处借助偏函数,
buttons[i][j].grid(row=i, column=j, )
# 借助网格布局,布置出细胞网络
return buttons
def creatMenu(window, buttons, lock): # 创建菜单栏
menubar = tk.Menu(window)
filemenu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label='选项', menu=filemenu) # 选项菜单栏
def start(): # 开始功能
try:
lock.release()
except:
pass
def pause(): # 暂停功能
lock.acquire()
def reseat(): # 重置功能
lock.acquire()
for i in buttons:
for j in i:
j['bg'] = 'white'
def quit(): # 退出功能
CellLife.stop = True
window.quit()
def save(): # 保存功能
with open('image.txt', 'w') as f:
for x in range(len(buttons)):
for y in range(len(buttons[x])):
if buttons[x][y]['bg'] == 'black':
f.write('[' + str(x) + ',' + str(y) + '],')
# 将个项功能放在菜单栏中,就是装入那个容器中
filemenu.add_command(label='开始', command=start)
filemenu.add_command(label='暂停', command=pause)
filemenu.add_command(label='重置', command=reseat)
filemenu.add_command(label='保存', command=save)
filemenu.add_command(label='退出', command=quit)
setmenu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label='设置', menu=setmenu)
speedmenu = tk.Menu()
setmenu.add_cascade(label='变化速度', menu=speedmenu)
# 将二级菜单添加到菜单中
def setSpeed(newspeed): # 调节速度
CellLife.speed = newspeed
# 将几个预设速度添加到二级菜单中,此处仍借助偏函数实现
speedmenu.add_command(label='慢', command=functools.partial(setSpeed, newspeed=2))
speedmenu.add_command(label='较慢', command=functools.partial(setSpeed, newspeed=1.5))
speedmenu.add_command(label='中等', command=functools.partial(setSpeed, newspeed=1))
speedmenu.add_command(label='快', command=functools.partial(setSpeed, newspeed=0.5))
speedmenu.add_command(label='极快', command=functools.partial(setSpeed, newspeed=0.1))
imgmenu = tk.Menu()
setmenu.add_cascade(label='预设图案', menu=imgmenu)
def setImg(locs): # 修改初始图案
for x in buttons:
for y in x:
y['bg'] = 'white'
for l in locs:
buttons[l[0]][l[1]]['bg'] = 'black'
def randImg(): # 随机初始图案
for x in buttons:
for y in x:
if random.choice((True, False)):
y['bg'] = 'black'
else:
y['bg'] = 'white'
# 几个预设的图案
loc1 = [[3, 25], [3, 28], [4, 24], [5, 24], [5, 28], [6, 24], [6, 25], [6, 26], [6, 27], [12, 21], [12, 22],
[13, 20], [13, 21], [13, 22], [13, 23], [14, 19], [14, 20], [14, 22], [14, 23], [15, 20], [15, 21],
[20, 25], [20, 28], [21, 24], [22, 24], [22, 28], [23, 24], [23, 25], [23, 26], [23, 27]]
loc2 = [[0, 6], [0, 14], [0, 22], [1, 1], [1, 2], [1, 3], [1, 6], [1, 9], [1, 10], [1, 11], [1, 14], [1, 17],
[1, 18], [1, 19], [1, 22], [1, 25], [1, 26], [1, 27], [2, 6], [2, 14], [2, 22], [4, 2], [4, 10], [4, 18],
[4, 26], [5, 2], [5, 5], [5, 6], [5, 7], [5, 10], [5, 13], [5, 14], [5, 15], [5, 18], [5, 21], [5, 22],
[5, 23], [5, 26], [6, 2], [6, 10], [6, 18], [6, 26], [8, 6], [8, 14], [8, 22], [9, 1], [9, 2], [9, 3],
[9, 6], [9, 9], [9, 10], [9, 11], [9, 14], [9, 17], [9, 18], [9, 19], [9, 22], [9, 25], [9, 26], [9, 27],
[10, 6], [10, 14], [10, 22], [12, 2], [12, 10], [12, 18], [12, 26], [13, 2], [13, 5], [13, 6], [13, 7],
[13, 10], [13, 13], [13, 14], [13, 15], [13, 18], [13, 21], [13, 22], [13, 23], [13, 26], [14, 2], [14, 10],
[14, 18], [14, 26], [16, 6], [16, 14], [16, 22], [17, 1], [17, 2], [17, 3], [17, 6], [17, 9], [17, 10],
[17, 11], [17, 14], [17, 17], [17, 18], [17, 19], [17, 22], [17, 25], [17, 26], [17, 27], [18, 6], [18, 14],
[18, 22], [20, 2], [20, 10], [20, 18], [20, 26], [21, 2], [21, 5], [21, 6], [21, 7], [21, 10], [21, 13],
[21, 14], [21, 15], [21, 18], [21, 21], [21, 22], [21, 23], [21, 26], [22, 2], [22, 10], [22, 18], [22, 26],
[24, 6], [24, 14], [24, 22], [25, 1], [25, 2], [25, 3], [25, 6], [25, 9], [25, 10], [25, 11], [25, 14],
[25, 17], [25, 18], [25, 19], [25, 22], [25, 25], [25, 26], [25, 27], [26, 6], [26, 14], [26, 22]]
loc3 = [[0, 14], [1, 13], [1, 15], [2, 12], [2, 14], [2, 16], [3, 11], [3, 13], [3, 15], [3, 17], [4, 10], [4, 12],
[4, 14], [4, 16], [4, 18], [5, 9], [5, 11], [5, 13], [5, 15], [5, 17], [5, 19], [6, 8], [6, 10], [6, 12],
[6, 14], [6, 16], [6, 18], [6, 20], [7, 7], [7, 9], [7, 11], [7, 13], [7, 15], [7, 17], [7, 19], [7, 21],
[8, 6], [8, 8], [8, 10], [8, 12], [8, 14], [8, 16], [8, 18], [8, 20], [8, 22], [9, 5], [9, 7], [9, 9],
[9, 11], [9, 13], [9, 15], [9, 17], [9, 19], [9, 21], [9, 23]]
# 将预设图案添加到二级菜单中
imgmenu.add_command(label='太空舰队', command=functools.partial(setImg, locs=loc1))
imgmenu.add_command(label='广场舞', command=functools.partial(setImg, locs=loc2))
imgmenu.add_command(label='百变脸谱', command=functools.partial(setImg, locs=loc3))
imgmenu.add_command(label='随机图案', command=randImg)
def instruction(): # 添加说明
tkinter.messagebox.showinfo(title='细胞自动机说明', message="细胞自动机介绍:\n"
" 细胞自动机(cellular automata)是为模拟包括自组织结构在内的复杂现象提供的一个强有力的方法,"
"也称为元胞自动机(Cellular Automaton)。细胞自动机模型的基本思想是:自然界里许多复杂结构和过程,"
"归根到底只是由大量基本组成单元的简单相互作用所引起。细胞自动机主要研究由小的计算机或部件,"
"按邻域连接方式连接成较大的、并行工作的计算机或部件的理论模型。它分为固定值型、周期型、混沌型"
"以及复杂型。(摘自百度百科)\n细胞自动机规则:\n 每一个小格代表一个细胞,灰色代表死亡,黑色"
"代表存活,一个细胞周围八个细胞中有2个细胞为存活状态时,细胞保持原状,当周围为3个存活细胞时,该细"
"胞变为或保持存活状态,其余情况均变为或保持死亡状态。\n操作说明:\n 初始状态下细胞全部为死亡状态,"
"点击任意细胞可改变其状态。细胞设置完成后点击选项→开始,即可开始运行。使用暂停功能时,细胞自动机暂停"
"到某一状态。点击重置可以使自动机重置为全部死亡状态。保存功能可将当前所有存活细胞的坐标保存到本地。\n "
"设置选项下,可以调节细胞自动机的运行速度以及使用预设图案和随机图案。\n关于软件及作者:"
"\n 软件开源,仅供读者交流学习之用。\n 作者LittleHuang,邮箱:2387143434@qq.com,欢迎来信交流。")
menubar.add_command(label='说明', command=instruction)
window.config(menu=menubar)
def dieOrLife(buttons, length, data): # 判断每一步细胞生死的功能函数
life = []
die = []
# print('running')
for i in range(length):
for j in range(length):
if buttons[i][j]['bg'] == 'black':
data[i][j] = 1
else:
data[i][j] = 0
for i in range(length):
for j in range(length):
sum = 0
for l in range(-1, 2):
for m in range(-1, 2):
if l + i >= 0 and m + j >= 0 and l + i < length and m + j < length:
sum += data[i + l][j + m]
sum -= data[i][j]
# print(sum)
if sum == 2 or sum == 3:
if sum == 3: # 繁殖
life.append([i, j])
else:
die.append([i, j])#死亡
if len(life) == 0 and len(die) == 0:
return
if len(life) != 0:
for l in life:
buttons[l[0]][l[1]]['bg'] = 'black'
if len(die) != 0:
for d in die:
buttons[d[0]][d[1]]['bg'] = 'white'
class CellLife(): # 由于需要调节速度,故借助类的属性实现
speed = 1
stop = False
def __init__(self, buttons):
self.length = len(buttons)
self.data = [[0 for i in range(self.length)] for j in range(self.length)]
def cellLife(self):
while True:
if self.stop:
return
lock.acquire()
dieOrLife(buttons, self.length, self.data)
lock.release()
time.sleep(self.speed)
if __name__ == '__main__':
window = tk.Tk() # 创建窗口
window.title('自动细胞机') # 窗口标题
window.geometry('754x754') # 这里的乘是小x
buttons = creatButtons(window, 29) # 创建窗口并制定方格数目
lock = threading.RLock() # 借助线程锁机制实现暂停开始功能,防止用户失误多次申请,使用RLock
lock.acquire()
cellLife = CellLife(buttons)
dataThread = threading.Thread(target=cellLife.cellLife)
# 由于UI界面,必须需要一个线程循环,因此使用多线程,另开一线程进行计算细胞状态
creatMenu(window, buttons, lock) # 创建菜单
dataThread.start() # 先开启运算线程,由于信号量已经被申请,故不会直接运行而会等待用户按下开始
# winThread=threading.Thread(target=window.mainloop)
window.mainloop() # 主窗口函数
# winThread.run()
效果展示
细胞自动机
友情链接
推荐一篇蛮不错的python tkinter学习博客: