非程序员的萌新,第一次学react,若有谬误,欢迎指正
完成官方入门教程
按照react官方的入门教程实现了一个三连棋(井字棋),实现的功能有:
- tic-tac-toe(三连棋)游戏的所有功能
- 能够判定玩家何时获胜
- 能够记录游戏进程
- 允许玩家查看游戏的历史记录,也可以查看任意一个历史版本的游戏棋盘状态
官方也留下一些课外作业:
- 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
- 在历史记录列表中加粗显示当前选择的项目。
- 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
- 添加一个可以升序或降序显示历史记录的按钮。
- 每当有人获胜时,高亮显示连成一线的 3 颗棋子。
- 当无人获胜时,显示一个平局的消息。
开始做作业
首先所需文具
- nodejs
- react
第一题
在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
这里说历史记录中每一步的坐标,那我们就在历史记录上动手脚,让每次点击(下子)都记录棋子的坐标。
在game的处理点击事件的函数handleClick中增加记录坐标lastMoves
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
lastMoves: {
player: squares[i],
row: parseInt(i / Options.boardRow) + 1,
col: i % Options.boardCol + 1,
}
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
lastMoves是一个对象,记录落子的选手和落子的坐标(行,列)
这里的Options.boardRow是第三题的一部分,这里先不细说,它代表着棋盘的行数,井字棋里的棋盘是3x3,所以这里实际上就是parseInt(i / 3) + 1,因为i的取值范围为0-8,并且棋盘上共有三列,所以i每隔3就代表着换行0-2,3-5,6-8,这样i/3 再用parseInt取整得到的就是0,1,2代表着行数,但我们坐标的需要从1开始所以要都加上1。获取落子的行号就是parseInt(i / Options.boardRow) + 1,列号就不细说了都差不多。
目前为止我们已经能够记录每下一步棋的棋子坐标以及下棋的人,剩下的就是把它显示出来
我们找到显示坐标的地方,就是Game中渲染历史记录的Rander里的moves
const moves = history.map((step, move) => {
const desc = move ? `player:${step.lastMoves.player},moves(row,col):(${step.lastMoves.row},${step.lastMoves.col})` : 'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)} className={'historyBtn ' + (move === this.state.stepNumber ? 'active' : '')}>{desc}</button>
</li>
);
})
这里教程有留下一个变量没用上,现在刚好可以用到step
这个代表这历史记录里的每一步棋盘,也包含了我们刚刚加的落子坐标对象,显示出来就可以。这样第一题就做完了~
第二题
在历史记录列表中加粗显示当前选择的项目。
加粗涉及到样式,个人不太喜欢在脚本里直接操作DOM和style,所以我们要在css中添加加粗样式
在渲染的时候通过变量来控制显示class。
index.css
.historyBtn {
border: none;
background: #fff;
}
.historyBtn.active {
font-weight: bold;
}
.historyBtn:hover {
color: royalblue;
cursor: pointer;
}
.historyBtn作为项目的默认样式,附加.active作为当前选中的样式。
然后还是在刚刚js的moves中把样式加进去。通过判断stepNumber是不是当前步骤把附加的.active拼上去,记得class之间留空格。
<li key={move}>
<button onClick={() => this.jumpTo(move)} className={'historyBtn ' + (move === this.state.stepNumber ? 'active' : '')}>{desc}</button>
</li>
然后第二题完成,抬走,下一题~
第三题
使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
循环的方法很多,我选最麻烦的...这里仅供参考。
首先原教程的生成棋盘的方式是生成一个长度9的数组,但是格式我不喜欢,不够直观。
我将其改造成[[0,1,2][3,4,5][6,7,8]]这样的格式。每一行一个数组组成一个棋盘。
其次,3x3的棋盘这里3x3也是写死的,也给他改成配置的,到时候3x4,4x4的改下配置和胜利条件即可。
这里我添加了一个变量作为设置,包含棋盘的行数列数。
const Options = {
boardRow: 3,
boardCol: 3
}
棋盘的改造可能会带来一些后遗症,所以先实现一个格式化棋盘的方法formatBoardMap方便以后使用棋盘。
const formatBoardMap = (mapRow, mapCol, boardMap) => {
boardMap = boardMap || [...Array(mapRow * mapCol).keys()]
return [...Array(mapRow)].map((x, i) => boardMap.slice(i * mapCol, (i + 1) * mapCol))
}
接收三个参数,用作生成新棋盘的行数,列数,以及用作已有棋盘格式化的棋盘对象,其中行数列数必选,最后返回格式化好的棋盘。
接下来就是循环生成棋盘
formatBoardMap(this.props.boardMap[0], this.props.boardMap[1]).map((items, index) => {
return (
<div className="board-row" key={index}>
{items.map((item, i) => this.renderSquare(item, index + i))}
</div>
)
})
传入配置的行数列数生成棋盘,返回的是一个数组,通过数组的map循环生成行,在行内循环生成格子。
然后第三题也好了,抬走,下一题~
第四题
添加一个可以升序或降序显示历史记录的按钮。
这题超简单,不多哔哔
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(Options.boardRow * Options.boardCol).fill(null),
}],
stepNumber: 0,
xIsNext: true,
ascending: true
}
}
直接在Game的构造函数中,把排序状态加进state。这里用正序的布尔值标记排序状态。
然后在历史记录下面增加一个排序按钮(要加在上面也行...)
let ascending = this.state.ascending;
return (
<div className="game">
<div className="game-board">
<Board winSquare={winnerIndex} boardMap={[Options.boardRow, Options.boardCol]} squares={current.squares} onClick={i => this.handleClick(i)} />
</div>
<div className="game-info">
<div>{status}</div>
<ol>{ascending ? moves : moves.reverse()}</ol>
<button onClick={() => this.setState({ ascending: !ascending })}>
{ascending ? 'Ascending' : 'Descending'}
</button>
</div>
</div>
);
按钮的点击事件的就是改变这个排序的值,按钮的显示内容也根据这个值进行切换。
最后把<ol>列表中的显示内容更改下{ascending ? moves : moves.reverse()}使用数组的倒序方法。大功告成,抬走下一题!
第五题
每当有人获胜时,高亮显示连成一线的 3 颗棋子。
当无人获胜时,显示一个平局的消息。
五六两题我把它合成一题,都是判断胜利的。
- 显示平局的简单我们先做。
let status = winner ? 'Winner: ' + winner.winner : history.length > 9 ? `No Winner, for peace~` : 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
改造一下判断胜利的状态,如果没有胜利者判断一下棋盘是不是被填满了。也就是history.length > 9,如果棋盘满了那就显示平局
- 胜利时显示连线
这个比较复杂,首先整个游戏的组件从大到小分别是Game -> Board -> squares,在Game中我们很难对每个格子进行准确的操控,所以把操控权交到Board手上,因为Board负责生成格子,那么思路就是Game把获胜者的格子名单交给Board,由Board通知这些格子该变色了。那么我们开始。
首先格子要有接收变色信号和自己变色的能力
.square.winner {
color: red;
}
const Square = props => <button className={"square" + (props.isWin ? " winner" : "")} onClick={props.onClick}> {props.value} </button>;
isWin为格子和棋盘约定的暗号,棋盘通知格子isWin的时候,格子就开始变色。
然后就是棋盘在生成格子的时候要把这个约定植入到格子里,等之后棋盘点名了,格子就可以收到通知。当棋盘收到Game给到的胜利者格子名单时,棋盘对照一下名单通知给对应的格子。
renderSquare(i, k) {
let [a, b, c] = this.props.winSquare;
let isWin = i === a || i === b || i === c
return <Square isWin={isWin} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} key={k} />
}
实现Game给出胜利格子名单比较简单 只要改造一下计算胜利者的方法,把胜利的序号一起返回回来。
const calculateWinner = 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 {
winner: squares[a],
index: [a, b, c]
};
}
}
return null;
}
Game每次计算胜利者的时候判断时候有胜利者,有的话将胜利者以及格子序号给到名单winnerIndex
let winnerIndex = winner ? winner.index : [];
然后把名单通知给棋盘
<Board winSquare={winnerIndex} boardMap={[Options.boardRow, Options.boardCol]} squares={current.squares} onClick={i => this.handleClick(i)} />
交卷~
完整代码
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
const Options = {
boardRow: 3,
boardCol: 3
}
const calculateWinner = 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 {
winner: squares[a],
index: [a, b, c]
};
}
}
return null;
}
const formatBoardMap = (mapRow, mapCol, boardMap) => {
boardMap = boardMap || [...Array(mapRow * mapCol).keys()]
return [...Array(mapRow)].map((x, i) => boardMap.slice(i * mapCol, (i + 1) * mapCol))
}
const Square = props => <button className={"square" + (props.isWin ? " winner" : "")} onClick={props.onClick}> {props.value} </button>;
class Board extends React.Component {
renderSquare(i, k) {
let [a, b, c] = this.props.winSquare;
let isWin = i === a || i === b || i === c
return <Square isWin={isWin} value={this.props.squares[i]} onClick={() => this.props.onClick(i)} key={k} />
}
render() {
return (
<div>
{
formatBoardMap(this.props.boardMap[0], this.props.boardMap[1]).map((items, index) => {
return (
<div className="board-row" key={index}>
{items.map((item, i) => this.renderSquare(item, index + i))}
</div>
)
})
}
</div>
);
}
}
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(Options.boardRow * Options.boardCol).fill(null),
}],
stepNumber: 0,
xIsNext: true,
ascending: 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();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
lastMoves: {
player: squares[i],
row: parseInt(i / Options.boardRow) + 1,
col: i % Options.boardCol + 1,
}
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
})
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ? `player:${step.lastMoves.player},moves(row,col):(${step.lastMoves.row},${step.lastMoves.col})` : 'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)} className={'historyBtn ' + (move === this.state.stepNumber ? 'active' : '')}>{desc}</button>
</li>
);
})
let status = winner ? 'Winner: ' + winner.winner : history.length > 9 ? `No Winner, for peace~` : 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
let winnerIndex = winner ? winner.index : []
let ascending = this.state.ascending;
return (
<div className="game">
<div className="game-board">
<Board winSquare={winnerIndex} boardMap={[Options.boardRow, Options.boardCol]} squares={current.squares} onClick={i => this.handleClick(i)} />
</div>
<div className="game-info">
<div>{status}</div>
<ol>{ascending ? moves : moves.reverse()}</ol>
<button onClick={() => this.setState({ ascending: !ascending })}>
{ascending ? 'Ascending' : 'Descending'}
</button>
</div>
</div>
);
}
}
ReactDOM.render(
<Game />,
document.getElementById('root')
);
index.css
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol,
ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square.winner {
color: red;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
.historyBtn {
border: none;
background: #fff;
}
.historyBtn.active {
font-weight: bold;
}
.historyBtn:hover {
color: royalblue;
cursor: pointer;
}