使用react结合Immutable实现一道五子棋面试题(下)

一道面试题,要求如下:

  • 用web技术实现一个五子棋
  • 支持DOM和Canvas版本切换
  • 实现悔棋功能
  • 实现一个撤销悔棋功能

前文: 使用react结合Immutable实现一道五子棋面试题(上)
github: https://github.com/fenggu/gomoku

Canvas版开发

首先创建一个<canvas />标签,然后使用canvasapi来进行棋盘和棋子的绘制。canvas版本的棋盘数据也是来自storedom版共用一份数据,这样可以达到domcanvas版本无缝切换的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
renderBox = (gomoku) => {
let canvas = this.refs.canvas
if (canvas.getContext) {
let ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, 450, 450)
ctx.fillStyle = '#333';
ctx.beginPath()
for (let i = 0; i < 16; i++) {
ctx.moveTo(15, 30 * i + 15)
ctx.lineTo(435, 30 * i + 15)
ctx.stroke()
}
for (let i = 0; i < 16; i++) {
ctx.moveTo(30 * i + 15, 15)
ctx.lineTo(30 * i + 15, 435)
ctx.stroke()
}
for (let x = 0; x < gomoku.size; x ++) {
for (let y = 0; y < gomoku.get(x).size; y++) {
if (gomoku.get(x).get(y) !== 0) {
ctx.beginPath()
ctx.fillStyle = gomoku.get(x).get(y) === 1 ? '#fff' : '#000'
ctx.arc(15 + y * 30, 15 + x * 30, 15, 0, Math.PI*2, true)
ctx.fill()
ctx.fillStyle = gomoku.get(x).get(y) === 1 ? '#000' : '#fff'
ctx.arc(15 + y * 30, 15 + x * 30, 15, 0, Math.PI*2, true)
ctx.stroke()
ctx.closePath();
}
}
}
}
}

然后还是使用上面的gomoku()方法,只是改一下x y的获取方式改成点击事件的坐标。然后通过减去棋盘的offsetLeftoffsetTop然后除以棋子的宽度取整。

1
2
let y = Math.floor((e.pageX - e.target.offsetLeft) / 30)
let x = Math.floor((e.pageY - e.target.offsetTop) / 30)

效果图:
img

输赢判断

每一次下棋都会有一次输赢判断,会以点击坐标为原点然后遍历上下左右,右上到左下,左上到右下6个方向。如果有连续相同颜色的5个棋子则胜利。因为DOM版和Canvas版是共用同一套逻辑的输赢判断。因此这里我采用了高阶组件的方式来传入输赢判断的函数。
高阶组件goMokuMixin代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as goMokuAction from '../../actions/gomoku'
export default (ComposedComponent) => {
/* Redux bind */
function mapStateToProps (state) {
let index = state.get('index')
return {
index: index,
gomoku: state.get('gomoku').get(index),
win: state.get('win'),
size: state.get('gomoku').size
};
}
function mapDispatchToProps (dispatch) {
return {
actions: bindActionCreators(Object.assign({}, goMokuAction), dispatch)
}
}
@connect(mapStateToProps, mapDispatchToProps)
class goMokuMixin extends Component {
isWin = (x, y, isCurrent) => {
this.props.actions.changeWin({
role: 0,
size: null
})
let pieces = this.props.gomoku
let sum = {
lrCount: {
count: 1,
order: {
0: [-1, 0],
1: [1, 0]
}
},
udCount: {
count: 1,
order: {
0: [0, -1],
1: [0, 1]
}
},
luCount: {
count: 1,
order: {
0: [-1, 1],
1: [1, -1]
}
},
rdCount: {
count: 1,
order: {
0: [1, 1],
1: [-1, -1]
}
}
}
for (let co in sum) {
for (let o in sum[co].order) {
let constantX = sum[co].order[o][0]
let constantY = sum[co].order[o][1]
for (let i = 1; i < 5; i++) {
if ((x + constantX * i) >= 15 || (x + constantX * i) < 0 || (y + constantY * i) >= 15 || (y + constantY * i) < 0) {
break;
}
if (pieces.get(x + constantX * i).get(y + constantY * i) !== isCurrent) {
break;
}
sum[co].count++
this.isOverCount(sum[co].count)
}
}
}
}
isOverCount = (count) => {
let isCurrent = this.props.index % 2 === 0 ? 2 : 1
if (count >= 5) {
this.props.actions.changeWin({
role: isCurrent,
size: this.props.size
})
}
}
render() {
return (<ComposedComponent isWin={this.isWin} />);
}
}
return goMokuMixin
}

时光旅行

因为题目要求上有悔棋以及撤销悔棋功能,这里我们采用store里的index的更改,来实现整场对局的时光旅行。因为随着对局次数越来越多,棋盘的数据也会越来越大,我采用了Immutable来存储对局的棋盘。
gomokureducer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Immutable from 'immutable';
import { PUSH_GOMOKU, GOMOKU_RESTORE } from '../constants/actionTypes'

const createData = () => {
let boxs = []
for (let x = 0; x < 15; x++) {
boxs.push([])
for (let y = 0; y < 15; y++) {
boxs[x].push(0)
}
}
return [boxs]
}

const initialState = Immutable.fromJS(createData())
export default (state = initialState, action) => {
switch (action.type) {
case PUSH_GOMOKU:
return state.splice(action.index).push(action.moku)
case GOMOKU_RESTORE:
return initialState
default:
return state;
}
}

所以我们是以this.props.gomoku.get(index)的形式来提取当前对局数据。也就是说只要将index的值-1即是悔棋,+1则是撤销悔棋。同时利用这个,我们也可以顺便实现一个对局回放的功能。即是将index的值置0然后定时增加即刻。
img

投食二维码