• React项目(二):生命游戏


    引子

    这是16年最后的一个练手项目,一贯的感觉就是,做项目容易,写说明文档难。更何况是一个唤起抑郁感觉的项目,码下的每个字,心就如加了一个千斤的砝码。

    2016年,有些事我都已忘记,但我现在还记得。2.14那天我见到了六年没见面高中同桌浩哥。为了缓解闰土式的尴尬,我提议到车站附近公园走走。

    浩哥被北风吹得不停地发抖,而我为了抑制寒冷,一根接一根地抽烟。

    “这蚂蚁好大只。”

    “踩死了对这个种群也没什么影响!” 言罢,浩哥一脚踩死了这只蚂蚁。

    “如果我们放过它,它存在的价值是什么呢?”

    十年前,浩哥永远一副对现状不满的态度——唤起了我的正能量。这方面我们形成了若干共同认知,这是我们为什么能成为朋友。

    对现状不满,所以要变得更强。

    但是十年后,我们都在思考:“我是谁,我在哪里,我往何处去?”

    于是我们,聊起了哲学话题,谈起了读过的书——自私的基因,生命是什么,沦为民工小说的三体。

    当年我们分到一个了生化班,他为选了化学,而不是物理而后悔,在他看来,物理才是真正的理科。而我受他的影响,放弃生物最终选了更加理科的化学。

    结果他大学学了管理,我学了药学,最后陷入进生物的泥坑里。

    在谈话中,我一直试图诱使浩哥回答这个问题:岁月一年一年弄冷一个人,但你心中还有当初那份温暖吗?

    如果你想有价值地活下去,答案应该是肯定的。但是结局...

    根据维基百科条目 Conway's Game of Life(康威生命游戏),康威生命游戏是英国数学家约翰·何顿·康威在1970年发明的细胞自动机。

    给出一个m*n的细胞矩阵,每个细胞都有一个初始状态:生存(1)或死亡(0)。每个细胞的变化都与它周围8个细胞有关,规则如下:

    当前细胞为存活状态时,当周围存活细胞不到2个时, 该细胞变成死亡状态。(模拟生命数量稀少)

    当前细胞为存活状态时,当周围有2个或3个存活的细胞时, 该细胞保持原样。

    当前细胞为存活状态时,当周围有3个以上的存活细胞时,该细胞变成死亡状态。(模拟生命数量过多)

    当前细胞为死亡状态时,当周围恰好有3个存活细胞时,该细胞变成存活状态。 (模拟繁殖)

    请按照规则编写出可视化的Game of Life游戏,类似于下面这个示例:

    https://codepen.io/FreeCodeCamp/full/reGdqx/

    游戏可以以任意状态开始,用户可以暂停或者重置游戏,并且能够观察到当前游戏进行的世代


    组件的逻辑

    首先得分组件,当组件混乱时,用ps画个图是个很好的方法。就省得打太多字了。

    和井字棋项目一样,采用的是一维数组。

    最麻烦的还是GameBoard,为了方便,还是采用table布局。

    要求点击面板,相应的格子变绿。

    var GameBoard=React.createClass({
                renderSquare:function(index,value){
                    return (
                        <Square
    						index={index}
    						createLife={()=>this.props.createLife(index)}
    					    value={this.props.status[index]}
    						key={index.toString()} />
                    );
                },
                getSquare:function(rows,cols){
                    var index=rows*cols;
                    var arr=[];
                    for(var i=index;i<index+cols;i++){
                        arr.push(this.renderSquare(i));
                    }
                    return arr;
                },
                getBoardRow:function(rows,cols){
                    var arr=[];
                    for(var i=0;i<rows;i++){
                        arr.push(<tr key={i}>{this.getSquare(i,cols)}</tr>);
                    }
                    return arr;
                },
    
                render:function(){
                    return (
                        <tbody>
                            {this.getBoardRow(this.props.size.rows,this.props.size.cols)}
                        </tbody>
                    );
                }
            });
    
            var Square=React.createClass({
    			handleClick:function(){
    				this.props.createLife(this.props.index);
    			},
    
                render:function(){
    				if(this.props.value=='true'){
    					return (
    						<td style={{background:'green'}}></td>
    					);
    				}else{
    					return (
    	                    <td onClick={this.handleClick}></td>
    	                );
    	            }
    			}
            });
    

    状态

    毫无疑问,这些组件的最终状态是以Game为主的。所以状态放在最上层。

    那么需要哪些状态呢?一般来说,定时器里需要更新的都是状态。按钮相关都是状态

    而之所以做游戏信息面板可以把状态很好的可视化出来。

    1. 培养皿尺寸——初始为70*50

    2. 游戏代数

    3. 繁殖速度

    4. 是否开始

    5. 存活率曲线——用来存放历代存活率的数组。

      我曾经考虑过它是否作为状态,结果是应该,因为它是动态变化的。

    6. 主游戏面板的细胞状态。是个随机一维数组

    初始状态

    既然培养皿的细胞是一维数组,就可以做一个随机数,培养皿随机填充细胞,覆盖率为30%。如果为有细胞,值为"ture",反之为"false"

    var Game=React.createClass({
                getInitialState:function(){
    				// 把棋盘初始化为一个数组。阈值为70%。
    				var arr=Array(50*70);
    				for(var i=0;i<arr.length;i++){
    					arr[i]=Math.random();
    					if(arr[i]>0.7){
    						arr[i]='true';
    					}else{
    						arr[i]='false';
    					}
    				}
    				
                    return {
                        size:{
                            rows:50,
                            cols:70
                        },
    					generation:1,
                        status:arr,
    					bStart:false,
    					speed:200,
    					lifeCurve:[]
                    }
                },
      。。。。
    

    有了状态,把属性传下去就行了。


    算法

    前面的状态设置和传递都不是什么大的问题,写各种函数也不算麻烦。所以就省略了。

    而说到算法,其实当时第一时间就想到井字棋项目。无非是对每个格子做上下左右斜的判断。

    所要做的就是统计个数。

    但是培养皿可能没那么大,所以妨做多一个判断。

    比如index是3499。要判断index+1——但status的第3500项不存在,就让它减去status的长度3500.

    如果index是0,要判断index-1,可以让它在原基础上加上3500.

    还有一个问题,当一行结束后,index+1就跳到下一行了。也可以根据this.state.rows的值做判断(是否整除),在这里不是很大的影响,就不判断了。

    好了。这就是定时器内最核心的算法了

    						this.setState(function(prev){
    							var status=prev.status.slice();
    							var _status=status.slice();
    
    							var rows=prev.size.rows;
    							var cols=prev.size.cols;
    							var total=cols*rows;
    
    							for(var i=0;i<status.length;i++){
    								var leftTop=status[i-cols-1]?i-cols-1:i-cols-1+total;
    								var top=status[i-cols]?i-cols:i-cols+total;
    								var rightTop=status[i-cols+1]?i-cols+1:i-cols+1+total;
    
    								var left=status[i-1]?i-1:i-1+total;
    								var right=status[i+1]?i+1:i+1-total;
    
    								var leftBot=status[i+cols-1]?i+cols-1:i+cols-1-total;
    								var bot=status[i+cols]?i+cols:i+cols-total;
    								var rightBot=status[i+cols+1]?i+cols+1:i+cols+1-total;
    
    								var around=
    									[
    										status[leftTop],
    										status[top],
    										status[rightTop],
    										status[left],
    										status[right],
    										status[leftBot],
    										status[bot],
    										status[rightBot]
    									];
    
    								var count=around.filter(function(_item){
    									return _item=='true';
    								}).length;
    
    								if(status[i]=='true'){
    									if(count<2){
    										_status[i]='false';
    									}else if(count==2||count==3){
    										_status[i]='true';
    									}else if(count>3){
    										_status[i]='false';
    									}
    								}else if(status[i]=='false'){
    									if(count==3){
    										_status[i]='true';
    									}
    								}
    							}
    
    							this.getLifeCurve()
    
    							return {
    								generation:prev.generation+1,
    								status:_status
    							}
    						});
    

    算法问题搞定,看看还有哪些坑。

    事实上我觉得坑是最有价值的部分

    定时器

    我遭遇到最大的坑是定时器。

    原来的定时器是放在componentDidMount里面的。但是我后来怀疑了。

    之所以有这个怀疑,还是基础javascript的问题。因为点击切换速度,值已经改变了但是定时器一旦打开就按照既定计划执行了。间隔时间改变不了。

    所以采用这样的写法

    var _this=this;
    (function resetTimer(){
      _this.timer=setInterval(function(){
        //balabala...
        clearInterval(_this.timer);
        resetTimer();
      },_this.state.speed)
    })();
    

    简单的生命曲线

    生命曲线图的展示属于数据可视化的内容,但实际上只要把这个数组拿到手,怎么显示其实就是其它技术的问题了。

    实际上,这里采用的是简洁的方法,就是在黑色背景下累加高度为1px的白色div,并把生存率乘以100再四舍五入。

    把这个div的宽度作为尚存率的显示数值。

    每次渲染的时候就把这个数组放到黑色背景中。

    getLifeCurve:function(){
    				var lifeCurve=this.state.lifeCurve.slice();
    				if(lifeCurve.length>500){
    					return false;
    				}//大于500步时记录终止。
    
    				var covered=parseInt(this.state.status.slice().filter(function(item){
    					return item=='true';
    				}).length/this.state.status.length*10000)/100;
    				lifeCurve.push(covered);
    				this.setState({
    					lifeCurve:lifeCurve
    				});
    			},
    

    结束

    demo地址:http://s.codepen.io/dangjingtao/debug/qqeXyz

    活着就是对抗熵增原理的过程。

    在这个规则下的生命看起来太残酷了,最后覆盖率大多在3%左右。但是只有这样,才有了各种图案。

    2017,愿每个有梦想的人,无悔地怒放出绚烂的生命之花。

  • 相关阅读:
    带你玩转Flink流批一体分布式实时处理引擎
    都2022年了,你的前端工具集应该有vueuse
    云图说|图解DGC:基于华为智能数据湖解决方案的一体化数据治理平台
    面试官: Flink双流JOIN了解吗? 简单说说其实现原理
    4种Spring Boot 实现通用 Auth 认证方式
    这8个JS 新功能,你应该去尝试一下
    Scrum Master需要具备哪些能力和经验
    dart系列之:时间你慢点走,我要在dart中抓住你
    dart系列之:数学什么的就是小意思,看我dart如何玩转它
    dart系列之:还在为编码解码而烦恼吗?用dart试试
  • 原文地址:https://www.cnblogs.com/djtao/p/6243125.html
Copyright © 2020-2023  润新知