react井字棋游戏改进列表实现

1,123 阅读4分钟

react官方文档提供了一个井字棋游戏教程,文档末尾提供了6条改进建议,本篇文章记录我对这些改进的实现。

1.在游戏历史记录列表显示每一步棋的坐标

在Game组件的history变量中维护一个名为position的对象,记录到达该棋盘状态的落棋位置坐标。

constructor(props){
       super(props);
       this.state = {
           history: [{
               squares: Array(9).fill(null),
               position: {row: -1, col: -1},
           }],
           xIsNext: true,
           stepNumber: 0,
       }
}

修改handleClick()函数,每下一步,加入落棋位置position

handleClick(i) {
        const history = this.state.history.slice(0, this.state.stepNumber + 1);
        let cur = history[history.length-1];
        if(cur.squares[i]!=null || calculateWinner(cur.squares)!=null){
            return;
        }
        const squares = cur.squares.slice();
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        //获取该索引对应的坐标
        let position = getPosition(i); 
        this.setState({ 
            history: history.concat({squares,position}), 
            xIsNext: !this.state.xIsNext,
            stepNumber: this.state.stepNumber+1,
        });
}

getPosition函数的作用是根据索引index获取其在棋盘上的行和列

function getPosition(index){
    let row = Math.trunc(index/3);
    let col = index%3;
    return {row,col};
}

js知识回顾 - Number类型的舍入

  • Math.trunc(IE 浏览器不支持这个方法)移除小数点后的所有内容而没有舍入:3.1 变成 3,-1.1 变成 -1。
  • Math.floor 向下舍入:3.1 变成 3,-1.1 变成 -2。
  • Math.ceil 向上舍入:3.1 变成 4,-1.1 变成 -1。
  • Math.round 向最近的整数舍入:3.1 变成 3,3.6 变成 4,中间值 3.5 变成 4。

js知识回顾-对象拷贝

  • slice()方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。

最后,修改Game组件的render函数,在历史记录<li>标签中写入落棋位置。

render() {
        //...省略其他代码
        let history = this.state.history.slice(0, this.state.stepNumber + 1);
        const moves = this.state.history.map((step,move)=>{
            const desc = move?'go to move # '+move:'go to game start';
            const pos = move?'row:'+this.state.history[move].position.row+',col:'+this.state.history[move].position.col:'';
            return (
                <li key={move}>{pos}<button onClick={()=>{this.jumpTo(move)}}>{desc}</button></li>
            )
        })
        //...
}

2.在历史记录列表中加粗显示当前选择的项目

在渲染历史记录信息时,map循环中新增一步判断当前move是否等于stepNumber,如果是的话,就把pos信息放入加粗标签中显示。

const moves = this.state.history.map((step,move)=>{
            const desc = move?'go to move # '+move:'go to game start';
            const pos = move?'row:'+this.state.history[move].position.row+',col:'+this.state.history[move].position.col:'';
            const posView = this.state.stepNumber === move?(<span className='bolder'>{pos}</span>):(<span>{pos}</span>);
            return (
                <li key={move}>{posView}<button onClick={()=>{this.jumpTo(move)}}>{desc}</button></li>
            )
});

设置bolder类的css样式

.bolder {
    font-weight: 900;
}

3.使用两个循环来渲染出棋盘的格子

首先修改Game组件的构造函数,初始化squares数组长度时动态赋值。然后修改render函数,向board组件传入棋盘的大小rowSize和colSize。

class Game extends React.Component{
    boardSize= {rowSize: 3, colSize:3};
    constructor(props){
        super(props);
        this.state = {
            history: [{
                squares: Array(this.boardSize.rowSize * this.boardSize.colSize).fill(null),
                position: {row: -1, col: -1},
            }],
            xIsNext: true,
            stepNumber: 0,
        }
    }
    //...
     render() {
        //...     
        return (
            <div className="game">
                <div className="game-board">
                    <Board rowSize={this.boardSize.rowSize} colSize={this.boardSize.colSize} squares={cur.squares} onClick={(i)=>{this.handleClick(i)}} />
                </div>
                ...
            </div>
        );
    }
}

最后修改board组件的渲染逻辑,根据行列长度使用两个循环来完成。

class Board extends React.Component {
    renderSquare(i) {
        return (<Square key={i} value={this.props.squares[i]} onClick={() => { this.props.onClick(i)}} />);
    }
    render() {
        const rows = [];
        for(let i=0;i<this.props.rowSize;i++){
            const row = [];
            for(let j=0;j<this.props.colSize;j++){
                row.push(this.renderSquare(this.props.colSize*i + j));
            }
            rows.push(
                <div key={i} className="board-row">{row}</div>
            );
        }
        return (
            <div>{rows}</div>
        );
    }
}

4.添加一个可以升序或降序显示历史记录的按钮

首先在Game组件的state中维护一个名为isAsc的变量,记录当前的历史记录显示顺序。

constructor(props){
        super(props);
        this.state = {
            history: [{
                squares: Array(this.boardSize.rowSize * this.boardSize.colSize).fill(null),
                position: {row: -1, col: -1},
            }],
            xIsNext: true,
            stepNumber: 0,
            isAsc: true,
        }
}

然后修改render函数中历史记录的渲染逻辑。保存一个moves数组的反转数组reverseMoves,根据isAsc的值,切换在页面中显示的数据是moves还是reverseMoves。

changeSeq(){
        this.setState({
            isAsc: !this.state.isAsc,
        })
}
render(){
        //...
        const reverseMoves = moves.slice().reverse();
        const movesView = this.state.isAsc?moves:reverseMoves;
        const seqButtonInfo =  this.state.isAsc?"change to DESC":"change to ASC";
        return (
            <div className="game">
                <div className="game-board">
                    <Board rowSize={this.boardSize.rowSize} colSize={this.boardSize.colSize} squares={cur.squares} onClick={(i)=>{this.handleClick(i)}} />
                </div>
                <div className="game-info">
                    <div>{status}</div>
                    <button onClick={()=>{this.changeSeq()}}>{seqButtonInfo}</button>
                    <ol>{movesView}</ol>
                </div>
            </div>
        );
}

5.每当有人获胜时,高亮显示连成一线的 3 颗棋子

在history数组中新增一个名为winSquares的数组变量,记录棋盘上的相应位置的状态,连成线的三个棋子上的值设置为true。

constructor(props){
        super(props);
        this.state = {
            history: [{
                squares: Array(this.boardSize.rowSize * this.boardSize.colSize).fill(null),
                winSquares: Array(this.boardSize.rowSize * this.boardSize.colSize).fill(false),
                position: {row: -1, col: -1},
            }],
            xIsNext: true,
            stepNumber: 0,
            isAsc: true,
        }
}

修改handleClick()函数,落棋时复制一份winSquares。

handleClick(i) {
        //...
        const winSquares = cur.winSquares.splice();
        this.setState({ 
            history: history.concat({squares,winSquares,position}), 
            xIsNext: !this.state.xIsNext,
            stepNumber: this.state.stepNumber+1,
        });
        //...
}

修改calculateWinner()函数,返回获胜方以及连起来的三个棋子的位置。

function calculateWinner(squares) {
    //...
    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],squares:[a,b,c]};
        }
    }
    return null;
}

修改Game组件的render()函数,当有人获胜时,修改winSquares数组的状态。

render(){
    const winner = calculateWinner(cur.squares);
    if(winner){
        status = 'Winner:' + winner.winner;
        cur.winSquares[winner.squares[0]] = true;
        cur.winSquares[winner.squares[1]] = true;
        cur.winSquares[winner.squares[2]] = true;
    }
}

最后修改各组件的渲染逻辑,根据isWin值控制显示样式。

function Square(props) {
    const valueView = props.value==null?null:props.isWin?(<span className='winSquare'>{props.value}</span>):(<span>{props.value}</span>);
    return (
        <button className="square" onClick={() => { props.onClick() }}>
            {valueView}
        </button>
    );
}
<Board 
    rowSize={this.boardSize.rowSize} 
    colSize={this.boardSize.colSize} 
    squares={cur.squares} 
    winSquares={cur.winSquares} 
    onClick={(i)=>{this.handleClick(i)}} 
/>
renderSquare(i) {
        return (
        <Square 
            key={i} 
            value={this.props.squares[i]} 
            isWin={this.props.winSquares[i]} 
            onClick={() => { this.props.onClick(i)}} 
        />
        );
    }

设置winSquare类的css样式。

.winSquare{
    color: red;
}

6.当无人获胜时,显示一个平局的消息

修改Game组件的render()函数,当前无人获胜时,判断stepNumber是否等于9,是的话说明当前棋盘已下满,该局为平局。

if(winner){
    status = 'Winner:' + winner.winner;
    cur.winSquares[winner.squares[0]] = true;
    cur.winSquares[winner.squares[1]] = true;
    cur.winSquares[winner.squares[2]] = true;
}
else if(this.state.stepNumber === 9){
    status = 'no one wins';
}
else{
    status = 'Next player: '+ (this.state.xIsNext?'X':'O');
}