前言
在学习新知识中的无聊之举,同时也是对java的一点复习。
本文正文两千五百字,加上代码一万字,还请各位mark赞评转一下多多支持作者君~
生命游戏
生命游戏是一种零玩家的数学游戏,它的前身是约翰·何顿·康威发明的细胞自动机(又称元胞自动机,细胞自动机是一种研究模型,生命游戏是一种数学游戏,所以我倾向于将它们视为两种东西)。
1 生命游戏的规则
如果一个生命,其周围的同类生命太少,会因为得不到帮助而死亡;如果太多,则会因为得不到足够的生命资源而死亡。 ——英国数学家约翰·康威
在一个有着n*m个格子的方形二维世界中,每个格子都居住着一个细胞。对于世界中的所有细胞而言,其有两个状态 活/死 ,每个细胞在每个时刻(回合)中在且仅在这两个状态中的一个下。
这个世界中所有细胞的状态会随着时刻的变化而变化,在每个时刻下,每个格子中的细胞会根据上一个时刻周围(也就是与其相邻)的格子中的细胞状态来决定。通常, 当周围有三个活细胞时,格子中的细胞为活;当周围有两个活细胞时,格子中的细胞维持原状;其他情况下,格子中的细胞死去。
2 生命游戏的意义
对于其前身细胞自动机而言,它可以模拟一些类似于组织结构的复杂现象。对于生命游戏本身而言,首先它是一个很典型的混沌系统,并且在数学和哲学领域上有所启迪;其次我认为它和分形一样是一种直观的显示数学之美的一种方式:因为原本杂乱无序的数据,在生命游戏的演化下有时会出现十分优美并且大多数情况下对称的形状。
对生命游戏进行抽象(属性部分)
1 建立基础的生命游戏类
我们可以将生命游戏发生的场景想象为一个棋盘,将细胞想象为一个棋子。在这个类比下,生命游戏与围棋有一定的相似性。
如果我们将生命游戏想象为一场棋类游戏,那么进行这场游戏必然需要棋盘和棋子。于是我们建立Board类表示棋盘,建立Cell类表示棋子。
对于Cell类,其必然有一个属性live,用于表示状态(生/死)。
对于Board类,作为一个抽象化的棋盘,其长度属性为height,宽度属性为width;它有一个二维数组,用于描述棋盘中的格子,这个二维数组名为board,成员为Cell类的对象。并且,棋盘并不是静止的,它会根据游戏的回合数发生变化。所以Board类有一个用于标记回合数的属性round。
在拥有棋盘(Board)和棋子(Cell)之后,我们还要有一个放置棋盘的场景(桌子),用于显示棋盘和棋子。创建Panel类用于表示生命游戏的场景(UI类)。Panel类中有若干辅助显示的Java窗口组件,表示棋盘的属性lifeboard,和用于表示游戏棋盘长度宽度的属性height与width。
2 特殊要求
除此之外,我还希望生命游戏在运行过程中能够记录自身所有回合中的情况,并且对当前回合的活细胞总数占棋盘比例、当前游戏中有过活细胞的总数占棋盘比例进行计数。
为了满足以上条件,我们给三个类分别添加以下属性:
对于Cell类,添加属性dict用于表示其是否已经存活过,添加列表属性list用于记录其所有存活的回合数。
对于Board类,添加属性live、dict,分别用于表示棋盘本回合存活细胞数量,棋盘所有存活过的细胞数量。
对于Panel类,添加几个用于调整显示回合数和进度的窗口组件。
最后,为了便于计算和显示,我们给Board类添加二维数组state用于表示上一回合的棋盘状态,添加属性count表示棋盘中的格子数量。
对生命游戏进行抽象(方法部分)
1 回合变动更新方法
之前我们讲到游戏本身会根据回合数发生变化,所以我们以1秒作为回合变动的单位。在此情况下,每变动一秒,让Panel类更新一次;而Panel类每更新一次,就会对其属性lifeboard进行一次更新;Board类每更新一次就会对其二维Cell数组中的所有成员进行更新。
如图所示,我们将三个类中用于更新的方法命名为update:
在一个回合中,从panel开始,依次调用Panel->Board->Cell*(count)的update方法,对整个游戏进行更新。
2 设置棋盘初值(初始态)方法
在逻辑上,我为“设置棋盘初值”创建了三个方法,它们分别是:
Board类下的set_cell,其输入一个设置为活的(初始状态下所有细胞皆为死)细胞的坐标,调用此细胞的update方法来给细胞赋值(此update调用时round为1,也就是回合一就是本游戏的初始态)。
Panel类下的set_board,它在Panel类初始化时调用,通过更改其内容可以更改游戏初始态的生成方式。
Panel类下的set_random,它默认在set_board中被调用,生成一个与方法内局部变量密度系数g有关的随机棋盘,g=[0…1],g越大,生成的棋盘中活细胞数量越少。
其实我并不建议读者使用随机生成方法生成棋盘,因为随机生成的棋盘并没有手动调用set_cell画出的棋盘精彩,通常随机棋盘产生不出什么有意思的图形。但是如果读者手动调用方法,哪怕是画个矩形都能出现很有意思的情况。set_random方法只是为读者测试而用。当然,如果读者想要研究棋盘中初始细胞的稠密程度与最终棋盘的情况的关系,还是可以调用这个方法的(这是个很有意思的研究,很可惜我没有这个时间和能力)。
3 绘制棋盘的方法
从源码中可以很容易地看出,棋盘的主UI界面在一个JPanel实例panel上,它是Panel类的一个属性。在绘制棋盘的过程中,我们使用panel的句柄在其上绘制图形,长宽都是10像素。
Panel类下的drawcell方法一次绘制一个细胞;Panel类下的draw方法调用drawcell对棋盘状态进行遍历,一次绘制整个棋盘。
源代码
代码采用的当然如标题所言是Java,使用的图形组件来自javax.swing.*。
1 Cell.java
游戏中细胞(棋子)的类。
import java.util.ArrayList;
public class Cell { //细胞类,是生命游戏中所有格子的类
public boolean dict; //是否有过活细胞
public boolean live; //本回合下是否有细胞(活着)
private ArrayList<Integer> list=new ArrayList<Integer>(); //活细胞回合数列表,其值为细胞存活的回合值
Cell(){
this.dict=false;
this.live=false;
this.list=new ArrayList<Integer>();
}
public boolean is_thisround(int round){ //返回一个表示此回合下是否有活细胞的bool
return this.list.contains(round);
}
public void update(boolean bool,int round){ //每回合之后更新细胞状态的函数
if(bool){
this.dict=true;
this.list.add(round);
}
this.live=bool;
}
}
2 Board.java
游戏中棋盘的类。
public class Board { //存放所有cell的棋盘类
private int width;
private int height; //棋盘的高度和宽度
public Cell[][] board; //细胞的二维数组
public int round; //回合数
private boolean state[][]; //此回合更新之前的细胞状态
public int live; //本回合存活的细胞数量
public int dict; //棋盘上所有存活过细胞的格子数量
public int count; //就是棋盘上格子的总数
Board(int width,int height){
this.round=;
this.live=;
this.dict=;
this.count=width*height;
this.width=width;
this.height=height;
this.board=new Cell[this.height][this.width];
this.state=new boolean[this.height][this.width];
for(int i=;i<this.height;i++){
for(int j=;j<this.width;j++){
this.board[i][j]=new Cell();
this.state[i][j]=false;
}
}
}
public void set_cell(int x,int y){ //设置初值 //不能不设置初值,不然生命游戏不会开始运转
this.board[x][y].update(true,this.round);
}
private int count_neighbor(int x,int y){ //返回此细胞周围活着的细胞数量
int[] m=new int[]{-,0,1};
int live=; //周围存活细胞计数
//修正数字
for(int i=;i<3;i++){
if(x+m[i]>=this.height|x+m[i]<){
continue;
}
for(int j=;j<3;j++){
if(y+m[j]>=this.width|y+m[j]<){
continue;
}
if(this.state[x+m[i]][y+m[j]]==true){ //相邻范围内的细胞是活着的计数就加
live++;
}
}
}
return live;
}
private void round_judge(int x,int y){ //判断并更新某细胞在此轮的状态
//这里有个问题,那就是更新是一次更新整个棋盘还是一步一步更新。
//我们用state保存上个状态的棋盘,一个状态一个状态更新 这样可以保证棋盘更新的同步性
int live_count=; //复活计数
int normal_count=; //维持原状计数
int judge=this.count_neighbor(x, y);
if(judge==live_count){
this.board[x][y].update(true,this.round); //如果复活则复活状态更新
}
else if(judge==normal_count){
this.board[x][y].update(this.state[x][y],this.round); //维持原状则将state中的状态代入更新
}
else{
this.board[x][y].update(false,this.round);
}
}
private void to_state(){ //将Cell.live的值传给state,虽然没有什么复用的可能但是出于逻辑关系还是分开来写
for(int i=;i<this.height;i++){
for(int j=;j<this.width;j++){
this.state[i][j]=this.board[i][j].live;
}
} //应该一开始就用一维数组,另外设计一个一维转二维的函数,可以减少一定的代码量
}
public boolean[][] round_board(int round){ //这个方法返回传入回合的棋盘
boolean[][] state=new boolean[this.height][this.width];
for(int i=;i<this.height;i++){
for(int j=;j<this.width;j++){
state[i][j]=this.board[i][j].is_thisround(round);
}
}
return state;
}
public boolean[][] this_board(){ //这个方法返回本回合的棋盘 //当然,用上面的方法返回也可以,就是开销大一点
boolean[][] state=new boolean[this.height][this.width];
for(int i=;i<this.height;i++){
for(int j=;j<this.width;j++){
state[i][j]=this.board[i][j].live;
}
}
return state;
}
private int count_live(){ //计算board中存活的细胞数量
//因为我也不常用java,想知道它有类似块的结构吗?有的话这些使用for的代码会简洁很多
int count=;
for(int i=;i<this.height;i++){
for(int j=;j<this.width;j++){
if(this.board[i][j].live){count++;}
}
}
return count;
}
private int count_dict(){ //计算board中已经有过存活细胞的块数量
int count=;
for(int i=;i<this.height;i++){
for(int j=;j<this.width;j++){
if(this.board[i][j].dict){count++;}
}
}
return count;
}
public void update(){ //更新一轮
this.to_state(); //传值给state
for(int i=;i<this.height;i++){
for(int j=;j<this.width;j++){
this.round_judge(i, j); //对state中数据计算更新出新的board
//也就是,这里计算的是state数据,更新的是board,所以每一轮都要先把board的更改移到state中
}
}
this.live=this.count_live();
this.dict=this.count_dict(); //更新两个数据
this.round++; //计数增加
}
}
3 Panel.java
游戏整体界面的类,也是main函数所在的类。
import javax.swing.JButton;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class Panel { // 画板类,是本程序的UI部分
public JFrame main; // 主框架
public JPanel panel; // 画板部分
public JPanel _settingp; // 下方的设置面板
public JPanel _progressp; // 右边的进度条面板
public JPanel _roundp; // 上方的回合数面板
public JButton start; // 开始按钮
public JButton stop; // 暂停按钮
public JProgressBar cell; // 细胞进度(细胞/总区域)
public JProgressBar dict; // 细胞占领进度 (有过细胞的格子/总区域)
public JSlider round; // 回合数滑块 //这玩意先不赋最大最小值,它在按下暂停按键时才会出现。
public Board lifeboard; // 棋盘
public JTextArea num; //回合数文本框
private boolean run; // 是否运行中布尔值
private boolean ch; // 滑块是否更改的布尔值
public static final int Width =; //固定的高度和宽度
public static final int Height =;
Panel() {
// 棋盘等其他值的初始化
this.lifeboard = new Board(Width /, Height / 10); // 我们设置格子为80*60,也就是一个格子十像素
this.run = false;
this.ch=false;
// main窗口
this.main = new JFrame("生命游戏");
this.main.setSize(, 700); // 设置大小
this.main.setLayout(new BorderLayout()); // 设置布局为方位布局(虽然这是默认布局)
this.main.setLocation(, 0); // 设置显示位置
// panel画板
this.panel = new JPanel();
this.panel.setBackground(java.awt.Color.LIGHT_GRAY);
this.panel.setPreferredSize(new Dimension(Width, Height));
// 其实我还有更好的想法,那就是给新增的格子和老的格子加上不同的颜色,逐步渐进,到底反弹,肯定会更好看
this.main.add(this.panel, BorderLayout.CENTER); // 将画板加到中间
// _settingp用于设置的面板
this._settingp = new JPanel();
this.start = new JButton("开始");
this.start.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
run = true;
}
});
this.stop = new JButton("暂停");
this.stop.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
run = false;
}
});
this._settingp.setLayout(new FlowLayout()); // 流布局
this._settingp.add(this.start);
this._settingp.add(this.stop);
this.main.add(this._settingp, BorderLayout.SOUTH); // 设置面板放在下面
// _progressp用于进度条和滑块的面板
this.cell = new JProgressBar(, this.lifeboard.count);
JTextField cellt=new JTextField("本回合活细胞比例");
this.cell.setStringPainted(true);
this.dict = new JProgressBar(, this.lifeboard.count);
JTextField dictt=new JTextField("活细胞比例");
this.dict.setStringPainted(true); // 设置绘制百分比
this._progressp = new JPanel();
this._progressp.setPreferredSize(new Dimension(,700));
this._progressp.setLayout(new FlowLayout());
this._progressp.add(cellt);
this._progressp.add(this.cell);
this._progressp.add(dictt);
this._progressp.add(this.dict);
////滑块部分
this.round = new JSlider();
this.round.setMinimum();
this.round.setMajorTickSpacing();
this.round.setMinorTickSpacing();
this.round.setPaintLabels(true);
this.round.setPaintTicks(true);
this.round.setOrientation(SwingConstants.VERTICAL); // 设置滑块垂直
this.round.setPreferredSize(new Dimension(,450));
this.round.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
ch = true; // 当滑块值变动时,ch置true
}
});
this._progressp.add(this.round);
this.round.setVisible(false); // 初始不可见
this.main.add(this._progressp, BorderLayout.EAST); // 进度条面板放在右边
// _roundp用于显示回合数的面板
this.num=new JTextArea("回合");
this.num.setEditable(false);
this._roundp=new JPanel();
this._roundp.setLayout(new FlowLayout());
this._roundp.add(this.num);
this.main.add(this._roundp,BorderLayout.NORTH);
this.main.setVisible(true); // 可见
}
public void set_board(){ //给棋盘绘制初始细胞 //通过更改这个部分,决定一个生命游戏的初始态
this.set_random();
}
private void set_random(){ //随机生成一个棋盘,可在set_board中调用
double g=.89; //密度系数
for(int i=;i<60;i++){
for(int j=;j<80;j++){
if(Math.random()>g){
this.lifeboard.set_cell(i,j);
}
}
}
}
private void draw() { // 重绘棋盘的方法,在ch为假时按照lifeboard.board绘制棋盘,在ch为真时按照lifeboard.round_board()绘制棋盘
boolean[][] board;
if (ch == true) {
board = this.lifeboard.round_board(this.round.getValue());
} else {
board = this.lifeboard.this_board();
}
for (int i =; i < Height / 10; i++) {
for (int j =; j < Width / 10; j++) {
drawcell(i, j, board[i][j]);
}
}
}
private void drawcell(int x, int y, boolean live) { // 重绘棋盘的一个格子
Graphics j = this.panel.getGraphics(); // 获取上下文
if (live) {
j.setColor(java.awt.Color.black);
} else {
j.setColor(java.awt.Color.white);
}
j.fillRect(y *, x * 10, 10, 10); // 绘制图形
}
public void update() { // 这个方法一秒调用一次,为一回合
if (this.run == true) {
this.round.setVisible(false); // 运行中不能调用滑块
this.ch = false; // 所以更改标志也要置false
this.lifeboard.update(); // 更新棋盘
this.cell.setValue(this.lifeboard.live); // 更新进度条
this.dict.setValue(this.lifeboard.dict);
this.num.setText("回合"+this.lifeboard.round);
} else {
this.round.setMaximum(this.lifeboard.round); // 重设滑块最大值
this.round.setVisible(true);
}
this.draw(); // 重新绘制棋盘
}
public static void main(String args[]) {
Panel panel = new Panel();
panel.set_board(); //设置初始棋盘
while (true) {
try {
Thread.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
panel.update();
}
}
}