回顾
在上一篇里完成了一个有以下功能的xxoo棋
- 三连棋游戏的所有功能
- 能够判定玩家何时获胜
- 能够记录游戏进程
- 允许玩家查看游戏的历史记录,也可以查看任意一个历史版本的游戏棋盘状态
- 在游戏历史记录列表显示每一步棋的坐标
- 在历史记录列表中加粗显示当前选择的项目
- 棋盘的大小易于改变(仅需在代码中修改一个常量)
- 历史记录可以升序或降序显示
- 每当有人获胜时,高亮显示连成一线的 3 颗棋子
- 当无人获胜时,显示一个平局的消息
现在以此为基础,把他做成一个【五子棋】游戏!
修改设计
五子棋和xxoo棋有很多相似的地方,比如说双方轮流落子,在横竖斜方向有若干棋子连成一线的一方取胜,无人能够取胜则平局等,相同的部分不需要做改动,需要修改的部分包括
- 棋盘,需要将3×3的棋盘扩展成标准的15×15的棋盘,由于上述第7点,其实很容易做到
- 获胜条件,看似只是从3颗棋子变成了5颗棋子,但由于之前的算法采用的是枚举,在3×3的棋盘上获胜情况不超过10种(横3竖3斜2共8种),因此枚举是个不错的办法,但在15×15的棋盘则有成百上千种可能,枚举就不切实际了,所以需要重新设计算法,重新实现判胜方法
算法
五子棋的获胜条件是五子相连,而获胜那一步一定是那5颗棋子之一,不然这一步之前就已经获胜,所以我们的办法是以落子的格子为中心,朝上下左右左上左下右上右下八个方向向外寻找相连的棋子,如果有任一对角的方向相连棋子数超过4(算上落子本身的话就是5),就达成获胜条件
实现
所以我们不仅需要知道此时棋盘布局的信息,还需要知道此时落子的信息,好在在上述第5点那里我们已经去到了每步落子的坐标,拿来用就可以了,因此给calculateWinner()方法新增坐标参数x, y
function calculateWinner(squares, x, y) {
//在这里重写方法
}
squares传来的是一个一维数组,为了之后的计算,我们把它转化成一个二维数组board
//1D -> 2D
var board = [];
var n = 0;
for(var i=0; i<M; i++) {
board[i] = [];
for(var j=0; j<M; j++, n++) {
board[i][j] = squares[n];
}
}
坐标值也转换成程序员熟悉的0起始的,并确定落子是X还是O
x = x-1;
y = y-1;
let mark = board[x][y];
【注意】初始状态下(无人落子时),坐标值为null,在这里会报错,所以需要实现判断,如果是初始状态则直接返回无人胜利的结果
if(!x || !y) {
return {winner : null, lines : null};
}
接着我们还需要准备2个数组
-
lines[8],用来保存每个方向上上与落子相连的相同棋子的序号(非二维坐标)
-
goOn[8],用来记录每个方向上是否还有必要继续向下探索
var lines = [[], [], [], [], [], [], [], []];
var goOn= Array(8).fill(true);
准备工作就绪,开始计算,因为五子相连就算胜利,每个方向最多只需要向前探索4步,对于每一步探索,先确认是否有必要探索(goOn为真)以及是否超过边界,如果没有问题再判断该格子是否是相同棋子,如果是就计入lines内,不是就让goOn为假,以后都不会再继续探索该方向了
for(var step=1; step<=4; step++) {
//↑0
if(goOn[0] && x-step >= 0) {
if(board[x-step][y] === mark) {
lines[0].push((x-step)*M + y);
} else {
goOn[0] = false;
}
}
//↗ 1
if(goOn[1] && x-step >= 0 && y+step < M) {
if(board[x-step][y+step] === mark) {
lines[1].push((x-step)*M + y+step);
} else {
goOn[1] = false;
}
}
//→ 2
if(goOn[2] && y+step < M) {
if(board[x][y+step] === mark) {
lines[2].push(x*M + y+step);
} else {
goOn[2] = false;
}
}
//↘ 3
if(goOn[3] && x+step < M && y+step < M) {
if(board[x+step][y+step] === mark) {
lines[3].push((x+step)*M + y+step);
} else {
goOn[3] = false;
}
}
//↓ 4
if(goOn[4] && x+step < M) {
if(board[x+step][y] === mark) {
lines[4].push((x+step)*M + y);
} else {
goOn[4] = false;
}
}
//↙ 5
if(goOn[5] && x+step < M && y-step >= 0) {
if(board[x+step][y-step] === mark) {
lines[5].push((x+step)*M + y-step);
} else {
goOn[5] = false;
}
}
//← 6
if(goOn[6] && y-step >= 0) {
if(board[x][y-step] === mark) {
lines[6].push(x*M + y-step);
} else {
goOn[6] = false;
}
}
//↖ 7
if(goOn[7] && x-step >= 0 && y-step >= 0) {
if(board[x-step][y-step] === mark) {
lines[7].push((x-step)*M + y-step);
} else {
goOn[7] = false;
}
}
}
八个方向向前四步都探索完后,进行判断,输出结果
if(lines[0].length+lines[4].length >= 4) {
return {winner : mark, lines : [x*M+y].concat(lines[0]).concat(lines[4])};
} else if(lines[1].length+lines[5].length >= 4) {
return {winner : mark, lines : [x*M+y].concat(lines[1]).concat(lines[5])};
} else if(lines[2].length+lines[6].length >= 4) {
return {winner : mark, lines : [x*M+y].concat(lines[2]).concat(lines[6])};
} else if(lines[3].length+lines[7].length >= 4) {
return {winner : mark, lines : [x*M+y].concat(lines[3]).concat(lines[7])};
} else {
return {winner : null, lines : null};
}
这里的返回值格式和之前一样,winner内记入X或O或null(没有人获胜),lines内记入连成线的棋子编号(一维),没人获胜则是null
最后,因为参数个数发生变化,引用的地方需要适当改写,点击按钮时
if(calculateWinner(squares, current.row, current.col).winner || squares[i]) {
return;
}
还有game渲染时
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const result = calculateWinner(current.squares, current.row, current.col);
//之后都一样。。。
最终效果
总结
因为之前的代码比较干净,从xxoo棋到五子棋,我们只在两个地方做了修改,棋盘大小(M),获胜判断方法的实现及调用,其他的功能也一切正常,所以你又得到了一个有以下功能的五子棋!
-
五子棋游戏的所有功能
- 能够判定玩家何时获胜
- 能够记录游戏进程
- 允许玩家查看游戏的历史记录,也可以查看任意一个历史版本的游戏棋盘状态
- 在游戏历史记录列表显示每一步棋的坐标
- 在历史记录列表中加粗显示当前选择的项目
- 棋盘的大小易于改变(仅需在代码中修改一个常量)
- 历史记录可以升序或降序显示
- 每当有人获胜时,高亮显示连成一线的 5 颗棋子
- 当无人获胜时,显示一个平局的消息
参考资料
React 入门实例教程 - 阮一峰的网络日志www.ruanyifeng.com
最终版完整代码暂时贴在下方,之后可能会放进GitHub更便于查看。。先酱
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
//size of board
const M = 15;
function Square(props) {
return (
<button className="square" onClick={ props.onClick }>
{/* add a font color */}
<font color={props.color}>{props.value}</font>
</button>
);
}
class Board extends React.Component {
// add a props of font color
renderSquare(i) {
let color;
if(this.props.line && this.props.line.includes(i)) {
color = "red";
} else {
color = "black";
}
return (
<Square
key={i}
value={this.props.squares[i]}
color={color}
onClick={ () => this.props.onClick(i) }
/>
);
}
//rerender by loop
render() {
var n = 0;
let board = [];
for(var i=0; i<M; i++) {
var boardRow = [];
for(var j=0; j<M; j++, n++) {
boardRow.push(this.renderSquare(n));
}
board.push(<div className="board-row" key={i}>{boardRow}</div>);
}
return (
<div>{board}</div>
);
}
}
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
//3 -> M
squares: Array(M*M).fill(null),
//row and column
row: null,
col: null,
}],
stepNumber: 0,
xIsNext: true,
//order
startToEnd : true,
}
}
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
//changed caused by calculateWinner
if(calculateWinner(squares, current.row, current.col).winner || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history : history.concat([{
squares : squares,
//row and column
row: parseInt(i/M)+1,
col: i%M+1,
}]),
stepNumber : history.length,
xIsNext : !this.state.xIsNext,
});
}
jumpTo(step) {
this.setState({
stepNumber : step,
xIsNext : (step % 2) === 0,
})
}
//add a function for changing order
changeOrder() {
this.setState({
startToEnd : !this.state.startToEnd,
})
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
//changed caused by calculateWinner
const result = calculateWinner(current.squares, current.row, current.col);
const winner = result.winner;
const lines = result.lines;
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move + ' (' + step.row + ',' + step.col + ')'://row and column
'Go to game start';
//point current move
if(step === current) {
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}><strong>{desc}</strong></button>
</li>
);
} else {
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
}
});
//reverse moves
let orderableMoves;
if(this.state.startToEnd) {
orderableMoves = moves;
} else {
orderableMoves = moves.reverse();
}
//isMatch?
let status;
if(winner) {
status = 'Winner: ' + winner;
} else if(!winner && this.state.stepNumber === M*M) {
status = 'MATCH!';
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
{/* add a line */}
<Board
squares={current.squares}
line={lines}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
{/* add a button */}
<button onClick={() => {this.changeOrder()}}>↑↓</button>
<ol>{orderableMoves}</ol>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(
<Game />,
document.getElementById('root')
);
// function calculateWinnerWith3(squares) {
// const lines = [
// [0, 1, 2],
// [3, 4, 5],
// [6, 7, 8],
// [0, 3, 6],
// [1, 4, 7],
// [2, 5, 8],
// [0, 4, 8],
// [2, 4, 6],
// ];
// for (let i = 0; i < lines.length; i++) {
// const [a, b, c] = lines[i];
// if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
// //return an object with all info
// return {winner : squares[a], lines : lines[i]};
// }
// }
// return {winner : null, lines : null};
// }
function calculateWinner(squares, x, y) {
//before start
if(!x || !y) {
return {winner : null, lines : null};
}
//1D -> 2D
var board = [];
var n = 0;
for(var i=0; i<M; i++) {
board[i] = [];
for(var j=0; j<M; j++, n++) {
board[i][j] = squares[n];
}
}
//as array starts from zero
x = x-1;
y = y-1;
let mark = board[x][y];
var lines = [[], [], [], [], [], [], [], []];
var goOn = Array(8).fill(true);
for(var step=1; step<=4; step++) {
//↑0
if(goOn[0] && x-step >= 0) {
if(board[x-step][y] === mark) {
lines[0].push((x-step)*M + y);
} else {
goOn[0] = false;
}
}
//↗ 1
if(goOn[1] && x-step >= 0 && y+step < M) {
if(board[x-step][y+step] === mark) {
lines[1].push((x-step)*M + y+step);
} else {
goOn[1] = false;
}
}
//→ 2
if(goOn[2] && y+step < M) {
if(board[x][y+step] === mark) {
lines[2].push(x*M + y+step);
} else {
goOn[2] = false;
}
}
//↘ 3
if(goOn[3] && x+step < M && y+step < M) {
if(board[x+step][y+step] === mark) {
lines[3].push((x+step)*M + y+step);
} else {
goOn[3] = false;
}
}
//↓ 4
if(goOn[4] && x+step < M) {
if(board[x+step][y] === mark) {
lines[4].push((x+step)*M + y);
} else {
goOn[4] = false;
}
}
//↙ 5
if(goOn[5] && x+step < M && y-step >= 0) {
if(board[x+step][y-step] === mark) {
lines[5].push((x+step)*M + y-step);
} else {
goOn[5] = false;
}
}
//← 6
if(goOn[6] && y-step >= 0) {
if(board[x][y-step] === mark) {
lines[6].push(x*M + y-step);
} else {
goOn[6] = false;
}
}
//↖ 7
if(goOn[7] && x-step >= 0 && y-step >= 0) {
if(board[x-step][y-step] === mark) {
lines[7].push((x-step)*M + y-step);
} else {
goOn[7] = false;
}
}
}
if(lines[0].length+lines[4].length >= 4) {
return {winner : mark, lines : [x*M+y].concat(lines[0]).concat(lines[4])};
} else if(lines[1].length+lines[5].length >= 4) {
return {winner : mark, lines : [x*M+y].concat(lines[1]).concat(lines[5])};
} else if(lines[2].length+lines[6].length >= 4) {
return {winner : mark, lines : [x*M+y].concat(lines[2]).concat(lines[6])};
} else if(lines[3].length+lines[7].length >= 4) {
return {winner : mark, lines : [x*M+y].concat(lines[3]).concat(lines[7])};
} else {
return {winner : null, lines : null};
}
}