(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
编译好的文件,请点击附件下载,可以单机基于浏览器运行。所有源码已经托管到码云,访问地址:https://git.oschina.net/wallimn/rwne.git。把插件也传上去了,有点儿大。
- <!DOCTYPE HTML>
- <html>
- <head>
- <Meta charset="utf-8"/>
- <title><%= htmlWebpackPlugin.options.title%></title>
- </head>
- <body>
- <div id="go-container"></div>
- </body>
- </html>