引言
一道朋友的面试题,要求如下:
- 用web技术实现一个五子棋
- 支持DOM和Canvas版本切换
- 实现悔棋功能
- 实现一个撤销悔棋功能
看起来挺有意思的,便用react redux Immutable等技术自己实现了一个。
github: https://github.com/fenggu/gomoku
Store数据结构
因为要支持DOM和Canvas版本切换的切换,所以采用react这种单向数据流的框架再适合不过了,只需要让DOM和Canvas版本公用一套数据流,然后做好View层的显示就好了。所以首先我们先来定义我们的store。store对象结构如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24gomoku: [
[
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
],
index: 0, // 对局索引
win: { // 胜利状态
role: 0, // 胜利对象
size: null // 胜利时的棋盘数组长度
}
其中棋盘上的棋子数据以二维数组pieces[x][y]:value的形式存储,
0代表空,1代表白,2代表黑。store里的gomoku将每一步的“棋盘数据”都存储进数组里,并通过Immutable的API:gomoku.get(index)的形式读取。所以我们将每一步的序列索引index也存进store中,这样也极大的方便开发时间旅行功能。
因为随着下棋步数越来越多,gomoku里存储的数组也会越来越大。为了解决这个问题,一般的做法是使用 shallowCopy(浅拷贝)或 deepCopy(深拷贝)来避免被修改,但这样做造成了 CPU 和内存的浪费。
Immutable 可以很好地解决这些问题。Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗。
DOM版开发
快速搭建完基本框架后,就可以进行愉快的DOM版本开发了。(基本框架代码参考顶部项目GitHub)
绘制棋盘
通过了解五子棋棋盘是15x15的格子布局,所以我采用div堆叠的方式来绘制这个棋盘。1
2
3
4
5
6
7
8
9renderBox = () => {
let boxs = []
for (let i = 0; i < 14; i ++) {
for (let j = 0; j < 14; j++) {
boxs.push(<div key={`b-${i}-${j}`} className='gomoku-box'></div>)
}
}
return boxs
}
绘制棋子
然后是棋子的布置,我们知道五子棋是下在棋盘线路的交叉点上,因此这里我采用16x16个透明格子的形式将棋子的格子铺在棋盘上,通过错位而达到棋子定位的效果。这样同时也方便了点击事件绑定。1
2
3
4
5
6
7
8
9renderPieces = () => {
let boxs = []
for (let i = 0; i < 15; i ++) {
for (let j = 0; j < 15; j++) {
boxs.push(<Piece key={`${i}-${j}`} x={i} y={j} isWin={this.props.isWin} class='gomoku-box'></Piece>)
}
}
return (boxs)
}
render1
2
3
4
5
6
7
8
9
10
11
12render() {
return (
<div className="gomoku">
<div className="gomoku-pieces">
{ this.renderPieces() }
</div>
<div className="gomoku-back">
{ this.renderBox() }
</div>
</div>
);
}
棋子组件
每一个棋子都是一个Piece小组件,Piece接受x y两个参数用来确定棋子的位置,在触发点击事件的同时,会将x y参数组成一个新的棋盘,并且通过dispatch action来更新棋盘的数据。1
2
3
4
5
6
7
8
9
10
11goMoku = () => {
// 黑: 2, 白: 1, 空: 0
let x = this.props.x
let y = this.props.y
if ( this.props.gomoku.get(x).get(y) !== 0 || this.props.win.role !== 0) return
let isCurrent = this.props.index % 2 === 0 ? 2 : 1
let newGoMoku = this.props.gomoku.setIn([x, y], isCurrent)
this.props.actions.pushGoMoku(newGoMoku, this.props.index + 1)
this.props.actions.changeGoMokuIndex(this.props.index + 1)
this.props.isWin(x, y, isCurrent)
}
同时为了防止棋子的重复渲染我们在Piece的componentWillReceiveProps中加入条件判断。1
2
3
4
5
6
7componentWillReceiveProps (nextProps) {
let x = this.props.x
let y = this.props.y
if (nextProps.gomoku.get(x).get(y) == this.props.gomoku.get(x).get(y)) {
return false
}
}
效果图:
下文: 使用react结合Immutable实现一道五子棋面试题(下)