(1)开始时,对javascript的对象或数组拷贝、赋值理解不是很透,折磨了我好长时间。 理解了对象或数组的赋值,实际上相当于C语言中的指针地址赋值,就知道了保存每一步的棋盘状态,要把对象拷贝一个副本,避免后继的变化,影响保存的状态。
(2)JQuery提供了对象拷贝的方法,extend。这个方法有深拷贝、浅拷贝之分,如果浅拷贝,不复制对象中的对象。还有个问题,就是数组拷贝后,会变成一个伪数组,能用下标取值,但不支持length属性。调这个错也用了很长时间。还好chrome支持断点调试。
目前支持功能:交替落子、布局摆子、撤销、重做、新建布局。界面如下图所示:
整个工程涉及四个文件。组件文件、状态管理文件、样式文件、网页文件。下面提供的是源码,依赖bootstrap、jquery。
一、状态管理文件GoStateManager.js
/** * 用于GO的状态管理。管理所有组件的状态,所有组件订阅事件,同步状态 * http://wallimn.iteye.com */ "use strict" var Events = require('events'); class GoStateManager { constructor() { this.initState(); this.initList(); this.eventEmitter = new Events.EventEmitter(); this.eventEmitter.setMaxListeners(500); var t1 = this.getDefaultPieceState(); var t2 = this.getDefaultPieceState(); }; //一个棋子的初始状态 getDefaultPieceState(){ return {visibility:'hidden',num:0,black:false}; } //所有棋子的初始状态 getDefaultPieces(){ var pieces=[];//所有棋子状态 for(var i=0 ; i<19*19; i++) pieces.push(this.getDefaultPieceState()); return pieces; } initState(){ //指示当前要下的子的状态,该状态使用后,调用next方法,切换状态 this.current= { index:1,//当前步数 goBlack:true,//是否是黑子,指行棋时 placeBlack:true,//是否是黑子,指布局时 numShow:false,//是否显示数字 place:false,//是否是布局摆子,如果是,不改变当前步数,布局时摆的棋子上面不显示数字 }; //所有棋子的状态 this.pieces = this.getDefaultPieces(); } //初始化重做、撤销两个队列 initList(){ this.undoList = [];//后进先出队列 this.redoList = [];//后进先出队列 } //将状态压栈,保存,保存的是对象的副本。 pushUndoList(current,pieces){ this.undoList.push({ current:this.cloneObject(current),pieces:this.cloneObject(pieces) }); } //清空RedoList,当执行下一步时执行此方法 clearRedoList(){ var len=this.redoList.length; if(len>0) this.redoList.splice(0,len); } //将状态压栈,保存,保存的是对象的副本。 pushRedoList(current,pieces){ this.redoList.push({ current:this.cloneObject(current),pieces:this.cloneObject(pieces) }); } //弹出队列中的元素,复制一个副本 popList(list) { var record = list.pop(); return { current: this.cloneObject(record.current),pieces: this.cloneObject(record.pieces) } } //输出链表内容,用于调试 printList(list){ for(var i=0; i<list.length; i++){ console.log("第%d步:%s",i,this.getVisiblePieces(list[i].pieces)); } } getVisiblePieces(pieces){ var info = ""; for(var j=0; j<19*19; j++){ if(pieces[j].visibility=='visible'){ info = info+pieces[j].num+','; } } //console.log("可见棋子序号:"+info); return info; } //撤销 undo(){ if (this.undoList.length==0){ console.log("不能撤销了!"); return; } //当前状态压入RedoList this.pushRedoList(this.current,this.pieces); var record = this.popList(this.undoList) this.current = record.current; this.pieces = record.pieces; this.pubCurrentChange(); this.pubPieceChange(); } //前进一步 redo(){ if (this.redoList.length==0){ console.log("不能前进了!"); return; } this.pushUndoList(this.current,this.pieces); var record = this.popList(this.redoList) this.current = record.current; this.pieces = record.pieces; //this.printList(this.undoList); //this.printList(this.redoList); this.pubCurrentChange(); this.pubPieceChange(); } //订阅当前状态变化事件 subCurrentChange(listener) { //console.log("订阅状态事件!"); this.eventEmitter.addListener('currentChange',listener); } //状态当前变化事件发生,通知监听器 pubCurrentChange(){ //console.log("发布状态事件"); this.eventEmitter.emit("currentChange",this.current); } //订阅棋子状态变化事件 subPieceChange(listener) { //console.log("订阅棋子事件!"); this.eventEmitter.addListener('pieceChange',listener); } //状态棋子变化事件发生,通知监听器 pubPieceChange(){ //console.log("发布棋子事件"); //传递数据,对解耦有一点儿帮助 this.eventEmitter.emit("pieceChange",this.pieces); } //推进当前状态到下一步 //这个函数内部调用 next(){ this.printList(this.undoList); if(this.current.place==true){ //如果是布局状态,不改变编号、颜色 } else{ this.current.index++; this.current.goBlack=!this.current.goBlack; } this.pubCurrentChange(); } //克隆对象 //数组被jquery复制后,变成了类数组(伪数组),不带有length方法,这个也比较坑 cloneObject(object){ return $.extend(true,{},object);//深层次复制。这个比较坑 } //是否处于布局状态 isPlace(){ return this.current.place==true; } //设置布局状态 setPlace(bBlack){ this.current.place=true; this.current.placeBlack=(bBlack==true); this.pubCurrentChange(); } //重新开始 restart(){ this.initState(); this.initList(); this.pubCurrentChange(); this.pubPieceChange(); } //返回当前的步数 getCurrentIndex(){ return this.current.index; } //返回当前状态 getCurrent(){ return this.current; } //返回所有棋子状态 getPieces(){ return this.pieces; } //设置先行方 setFirst(bBlack) { this.current.place = false; //仅处于第一步时,可以改变行棋的黑白颜色 if( this.current.index==1){ this.current.goBlack=(bBlack==true); } this.pubCurrentChange(); } //在棋上点击 //如果棋子状态变化,则返回为true,否则返回false clickPiece(index){ //每次下一步之前的状态都记下来,以便能够回退 this.pushUndoList(this.current,this.pieces); this.clearRedoList(); var state=this.pieces[index];//应该传递的是指针,相当于起了个别名,实际对应同一块内存地址 if (state.visibility=='visible' && this.isPlace()==false){//棋子可见、非布局状态 console.log("棋子可见、非布局状态,退出!"); return false; } if (state.visibility=='visible' && this.isPlace()==true && state.num!=0){//棋子可见、布局状态,且非布局棋子 console.log("棋子可见、布局状态,且非布局棋子,退出!"); return false; } //console.log('可以修改棋子状态'); if (this.isPlace()==false){//行棋中 state.num = this.current.index; state.black = this.current.goBlack; state.visibility = 'visible'; } else{//布局 state.num=0;//布局状态下,放的棋子,其数字设置为0 //如果原来棋子已经显示,且颜色相同,用是布局摆的棋子,设置其隐藏 if(state.visibility=='visible' && this.current.placeBlack==state.black && state.num==0){ state.visibility = 'hidden'; //棋子颜色不重要,下次再显示时,会设置颜色 } else{ state.black = this.current.placeBlack; state.visibility = 'visible'; } } this.pubPieceChange(); //StateManager.setPieceState(this.state.pieceId,state);//这里有点儿乱 //这个放最后,完成大大压栈工作 this.next(); //this.setState(state); return true; } } module.exports = new GoStateManager();
二、组件文件Go.js
//http://wallimn.iteye.com var React = require('react'); var ReactDOM = require('react-dom'); require('../../../css/go.css'); var StateManager = require('../../store/main/GoStateManager'); "user strick" //当前步状态指示器,可以指标当前步数、落子方、是否处理布局状态等信息 class CurrentLabel extends React.Component{ constructor(props){ super(props); //使用全局的状态作为初始状态 var current = StateManager.getCurrent(); this.state={ index:current.index,goBlack:current.goBlack,placeBlack:current.placeBlack,place:current.place,}; //设置currentChange函数的this this.currentChange=this.currentChange.bind(this); //注册事件监听器 StateManager.subCurrentChange(this.currentChange); } //状态改变事件监听器,调整组件的状态 currentChange(current){ this.setState({ index:current.index,}); } render(){ return <span className="bg-success"> <strong>当前步数:</strong>{this.state.index} <strong> 落子方:</strong>{this.state.goBlack==true?'黑方':'白方'} <strong> 布局子:</strong>{this.state.placeBlack==true?'黑子':'白子'} <strong> 状态:</strong>{this.state.place==true?'布局':'行棋'} </span>; } } //围棋桌面 class GoDesk extends React.Component { constructor(props) { super(props); this.state = { refresh: false }; } render() { var self = this; this.state.refresh=false; var pieces = []; //每个交叉点上都放一个子,只是未点击时不显示,棋子黑白、编号都不重要,用户点击时会修改 for (var i=0; i<19*19; i++){ pieces.push( <GoPiece black={i % 2==0 ?true:false} key={'go'+(i+1)} pieceId={i}/> ); } return <div className="go-desk"> <div className="go-opr"> <GoBtns /> </div> <div className="go-board"> {pieces} </div> <div className="text-center"> <CurrentLabel /> </div> </div>; } } //使用bootstap的按钮组,可以不用控制按钮的状态,较为方便,还没有完全走通 //使用radio按钮组实现几个控制行棋的按钮,因为只能处于其中一个状态 class GoBtns extends React.Component{ constructor(props){ super(props); this.state={index:1};//指标按钮的激活状态,没有完成 this.setFirstClickHandle=this.setFirstClickHandle.bind(this); this.setPlaceClickHandle=this.setPlaceClickHandle.bind(this); this.newClickHandle=this.newClickHandle.bind(this); this.saveClickHandle=this.saveClickHandle.bind(this); this.loadClickHandle=this.loadClickHandle.bind(this); this.redoClickHandle=this.redoClickHandle.bind(this); this.undoClickHandle=this.undoClickHandle.bind(this); } //这个还没有验证 getActiveBtnIndex(){ if (StateManager.current.place==false) return 1;//黑先、白先差别不大,似乎没有影响 else if (StateManager.current.placeBlack)return 2; else return 3; } //设置黑先 setFirstClickHandle(bBlack){ console.log("设置落子方颜色:"+bBlack); StateManager.setFirst(bBlack); } setPlaceClickHandle(bBlack){ console.log("设置布局子颜色:"+bBlack); StateManager.setPlace(bBlack); } newClickHandle(){ if (confirm('您确定要新建布局吗?')==true){ StateManager.restart(); } } saveClickHandle(){ alert("暂示实现"); } loadClickHandle(){ alert("暂示实现"); } redoClickHandle(){ StateManager.redo(); } undoClickHandle(){ StateManager.undo(); } render(){ return <div> <span> <div className="btn-group" data-toggle="buttons"> <label className="btn btn-sm btn-default active" onClick={this.setFirstClickHandle.bind(this,true)}><input type="radio" autoComplete="off" defaultChecked title="黑方先走" />黑先</label> <label className="btn btn-sm btn-default" onClick={this.setFirstClickHandle.bind(this,false)}><input type="radio" autoComplete="off" />白先</label> <label className="btn btn-sm btn-default" onClick={this.setPlaceClickHandle.bind(this,true)}><input type="radio" autoComplete="off" />黑子</label> <label className="btn btn-sm btn-default" onClick={this.setPlaceClickHandle.bind(this,false)}><input type="radio" autoComplete="off" />白子</label> </div> </span> <span> <button className="btn btn-sm btn-default" onClick={this.newClickHandle}>新建</button> <button className="btn btn-sm btn-default" onClick={this.saveClickHandle}>保存</button> <button className="btn btn-sm btn-default" onClick={this.loadClickHandle}>打开</button> </span> <span> <button className="btn btn-sm btn-default" onClick={this.undoClickHandle}>撤销</button> <button className="btn btn-sm btn-default" onClick={this.redoClickHandle}>重做</button> </span> </div>; } } //棋子 class GoPiece extends React.Component{ constructor(props){ super(props); var pieceId = props.pieceId; var pieceState = StateManager.getPieces()[pieceId]; this.state={ showNum:true,//是否显示数字,这个应该是个全局参数 num:pieceState.num,//子上显示的数字,如果为零,表示布局时摆的子,不显示数字 black:pieceState.black,//true表示为黑 last:false,//是否是最后一个子 pieceId:pieceId,//棋子的ID,左上为0,从左到右、从上到下,赋值后不发生变化 visibility:pieceState.visibility,//不可见时,为未放子或者被吃掉,从全局变量中取, } //设置this,很重要 this.handleClick=this.handleClick.bind(this); this.pieceChange=this.pieceChange.bind(this); StateManager.subPieceChange(this.pieceChange); } pieceChange(piecesArray){ //React会判断UI要不要更新,全部更新,不要紧 this.setState({ visibility:piecesArray[this.state.pieceId].visibility,black:piecesArray[this.state.pieceId].black,num:piecesArray[this.state.pieceId].num,showNum:StateManager.current.numShow,last:piecesArray[this.state.pieceId].num==StateManager.current.index,//没有想好如何判断 }); } //这个函数不直接改变自己组件的状态 handleClick(){ StateManager.clickPiece(this.state.pieceId); } render(){ var className="go-piece go-piece-"+(this.state.black==true?'black':'white'); //console.log(this.state); if (this.state.visibility=='hidden') className = className+" go-piece-hidden"; var pieceNum = this.state.num==0?'':this.state.num; return <div className={className} onClick={this.handleClick} id={'piece_'+this.state.pieceId}> <span style={{visibility:this.state.visibility}}>{pieceNum}</span> </div>; } } ReactDOM.render( <GoDesk />,document.getElementById('go-container') );
三、样式文件go.css
html,body{ height:100%; } .go-desk{ background-image:url(../img/go/bk.png); width:100%; height:100%; padding:20px; } .go-opr{ height:30px; text-align:center; margin-bottom:1em; } .go-opr span{ margin:0 0.5em; } .go-opr span button{ margin:0 0.05em; } .go-board{ width:800px; height:800px; margin:0 auto; background-image:url(../img/go/board.png); background-repeat:no-repeat; padding:20px; } .go-piece{ width:40px; height:40px; float:left; background-image:url(../img/go/piece.png); text-align:center; line-height:40px; vertical-align:middle; font-size:20px; } .go-piece span{ } .go-piece-white{ background-position:-40px 0; color:black; } .go-piece-black{ background-position:0 0; color:white; } .go-piece-hidden{ background-image:none; }
四、网页文件go.html
<!DOCTYPE HTML> <html> <head> <Meta charset="utf-8"/> <title><%= htmlWebpackPlugin.options.title%></title> </head> <body> <div id="go-container"></div> </body> </html>编译好的文件,请点击附件下载,可以单机基于浏览器运行。所有源码已经托管到码云,访问地址:https://git.oschina.net/wallimn/rwne.git。把插件也传上去了,有点儿大。