欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

React + Ts 实现三子棋小游戏

程序员文章站 2023-08-23 11:15:53
"在这里阅读效果更佳" 还记得当年和同桌在草稿纸上下三子棋的时光吗 今天我们就用代码来重温一下年少(假设你有react基础,没有也行,只要你会三大框架的任意一种,上手react不难) 游戏规则 + 双方各执一子,在九宫格内一方三子连成线则游戏结束 + 九宫格下满未有三子连线则视为平局 "你可以点击这 ......

在这里阅读效果更佳

还记得当年和同桌在草稿纸上下三子棋的时光吗

今天我们就用代码来重温一下年少(假设你有react基础,没有也行,只要你会三大框架的任意一种,上手react不难)

游戏规则

  • 双方各执一子,在九宫格内一方三子连成线则游戏结束
  • 九宫格下满未有三子连线则视为平局

准备阶段

建议先全局安装typescript 和 create-react-app(安装过请忽略

npm install typescript create-react-app -g

使用typescript初始化项目

create-react-app demo --typescript

初始化成功后ts环境已经配好了,不需要你手动加ts配置

此时就是tsx语法,我们就可以愉快的写ts了
src文件夹就是开发目录,所有代码都写在src文件夹下
我们使用sass来写样式,先安装sass

npm install node-sass --save

运行项目

npm run start

删掉初始化界面的一些代码

开发阶段

组件化

开发一个项目其实就是开发组件

把一个项目拆分一个个小组件,方便后期维护以及复用

  1. 棋子组件
  2. 棋盘组件
  3. 游戏规则组件
  4. 游戏状态组件

react中组件分为类组件和函数组件

需要管理状态的最好使用类组件
所以我们先把app改成类组件

import react from 'react';
import './app.css';
class app extends react.component{
    render(): react.reactelement<any, string | react.jsxelementconstructor<any>> | string | number | {} | react.reactnodearray | react.reactportal | boolean | null | undefined {
        return (
            <div classname="app">
            </div>
        );
    }
};

export default app;

开发棋子组件

在src下新建component文件夹,在component文件夹下新建chesscomp.tsx,chesscomp.css

以后我们的组件都放在component文件夹下
棋子组件我们使用函数组件,思考需要传入组件的属性的类型:

  1. type(棋子的类型)
  2. onclick(点击棋子触发的回调函数)

棋子类型有三种(红子 ,黑子, 空),

为了约束棋子类型,我们使用一个枚举类型,
在src下新建types文件夹,专门放类型约束,
在types下新建enums.ts约束棋子类型

export enum chesstype {
    none,
    red,
    black
}

并在棋子tsx中导入

传入tsx的所有属性用一个iprops接口约束

interface iprops {
    type: chesstype
    onclick?: () => void
}

全部tsx代码:

import react from 'react';
import {chesstype} from "../types/enums";
import './chesscomp.css';

interface iprops {
    type: chesstype
    onclick?: () => void
}
function chesscomp ({type, onclick}: iprops) {
    let chess = null;
    switch (type) {
        case chesstype.red:
            chess = <div classname="red chess-item"></div>;
        break;
        case chesstype.black:
            chess = <div classname="black chess-item"></div>;
        break;
        default:
            chess = null;
    }
    return (
        <div classname="chess" onclick={() => {
            if (type === chesstype.none && onclick) {
                onclick();
            }
        }}>
            {chess}
        </div>
    )
};
export default chesscomp;

其中棋子只有为none类型时才能被点击
scss 代码:
棋子我们用背景颜色径向渐变来模拟

$bordercolor: #dddddd;
$redchess: #ff4400;
$blackchess: #282c34;
.chess{
    display: flex;
    justify-content: center;
    align-items: center;
    width: 50px;
    height: 50px;
    border: 2px solid $bordercolor;
    box-sizing: border-box;
    cursor: pointer;
    .chess-item{
        width: 30px;
        height: 30px;
        border-radius: 50%;
    }
    .red{
        background: radial-gradient(#fff, $redchess);
    }
    .black{
        background: radial-gradient(#fff, $blackchess);
    }
}

开发棋盘组件

同理在component文件夹下新建boardcomp.tsx,boardcomp.scss

棋盘组件我们需要传递三个参数:

  1. 棋子的数组
  2. 游戏是否结束
  3. 点击事件函数

循环数组渲染棋子, 并给游戏是否结束一个默认值
全部tsx代码:

import react from 'react';
import {chesstype} from "../types/enums";
import chesscomp from "./chesscomp";
import "./boardcomp.scss";
interface iprops {
    chesses: chesstype[];
    isgameover?: boolean
    onclick?: (index: number) => void
}
const boardcomp: react.fc<iprops> = function(props) {
    // 类型断言
    const isgameover = props.isgameover as boolean;
    // 非空断言
    // const isgameover = props.isgameover!;
    const list = props.chesses.map((type, index) => {
        return (
            <chesscomp
                type={type}
                key={index}
            onclick={() => {
                if (props.onclick && !isgameover) {
                    props.onclick(index)
                }
            }}/>
        )
    });
    return (
        <div classname="board">
            {list}
        </div>
    )
};
boardcomp.defaultprops = {
  isgameover: false
};
export default boardcomp;

scss 代码:

使用flex布局

.board{
    display: flex;
    flex-wrap: wrap;
    width: 150px;
    height: 150px;
}

开发游戏规则组件

在component文件夹下新建game.tsx,game.scss

游戏规则组件不需要传参,我们使用类组件来管理状态
在types文件夹下的enums.ts里新增游戏状态的枚举类型

export enum chesstype {
    none,
    red,
    black
}
export enum gamestatus {
    /**
     * 游戏中
     */
    gaming,
    /**
     * 红方胜利
     */
    redwin,
    /**
     * 黑方胜利
     */
    blackwin,
    /**
     * 平局
     */
    equal,
}

核心的代码就是如何判断游戏的状态,我的方法有点死,你们可以自己重构,

import react from 'react';
import {chesstype, gamestatus} from "../types/enums";
import boardcomp from "./boardcomp";
import gamestatuscomp from "./gamestatuscomp";
import './game.scss';

/**
 * 棋子的数组
 * 游戏状态
 * 下一次下棋的类型
 */
interface istate {
    chesses: chesstype[],
    gamestatus: gamestatus,
    nextchess: chesstype.red | chesstype.black
}
class game extends react.component<{}, istate> {
    state: istate = {
        chesses: [],
        gamestatus: gamestatus.gaming,
        nextchess: chesstype.black
    };

    /**
     * 组件挂载完初始化
     */
    componentdidmount(): void {
        this.init();
    }
    /**
     * 初始化9宫格
     */
    init() {
        const arr: chesstype[] = [];
        for (let i = 0; i < 9; i ++) {
            arr.push(chesstype.none)
        }
        this.setstate({
            chesses: arr,
            gamestatus: gamestatus.gaming,
            nextchess: chesstype.black
        })
    }

    /**
     * 处理点击事件,改变棋子状态和游戏状态
     */
    handlechessclick(index: number) {
        const chesses: chesstype[] = [...this.state.chesses];
        chesses[index] = this.state.nextchess;
        this.setstate(prestate => ({
            chesses,
            nextchess: prestate.nextchess === chesstype.black? chesstype.red : chesstype.black,
            gamestatus: this.getstatus(chesses, index)
        }))
    }

    /**
     * 获取游戏状态
     */
    getstatus(chesses: chesstype[], index: number): gamestatus {
        // 判断是否有一方胜利
        const hormin = math.floor(index/3) * 3;
        const vermin = index % 3;
        // 横向, 纵向, 斜向胜利
        if ((chesses[hormin] === chesses[hormin + 1] && chesses[hormin + 1] === chesses[hormin + 2]) ||
            (chesses[vermin] === chesses[vermin + 3] && chesses[vermin + 3] === chesses[vermin + 6]) ||
            (chesses[0] === chesses[4] && chesses[4] === chesses[8] && chesses[0] !== chesstype.none) ||
            ((chesses[2] === chesses[4] && chesses[4] === chesses[6] && chesses[2] !== chesstype.none))) {
            return chesses[index] === chesstype.black ? gamestatus.blackwin : gamestatus.redwin;
        }
        // 平局
        if (!chesses.includes(chesstype.none)) {
            return gamestatus.equal;
        }
        // 游戏中
        return gamestatus.gaming;
    }
    render(): react.reactnode {
        return <div classname="game">
            <h1>三子棋游戏</h1>
            <gamestatuscomp next={this.state.nextchess} status={this.state.gamestatus}/>
            <boardcomp
                chesses={this.state.chesses}
                isgameover={this.state.gamestatus !== gamestatus.gaming}
                onclick={this.handlechessclick.bind(this)}/>
                <button onclick={() => {
                this.init()}
                }>重新开始</button>
        </div>;
    }
}

export default game;

样式

.game{
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-around;
  top: 100px;
  width: 250px;
  height: 400px;
  left: 50%;
  transform: translatex(-50%);
}

开发显示游戏状态的组件

这个组件用来显示状态,在component文件夹下新建gamestatus.tsx,gamestatus.scss

没什么好说的,直接上代码

import react from 'react';
import {chesstype, gamestatus} from "../types/enums";
import './gamestatus.scss';
interface iprops {
    status: gamestatus
    next: chesstype.red | chesstype.black
}
function gamestatuscomp(props: iprops) {
    let content: jsx.element;
    if (props.status === gamestatus.gaming) {
        if (props.next === chesstype.red) {
            content = <div classname="next red">红方落子</div>
        } else {
            content = <div classname="next black">黑方落子</div>
        }
    } else {
        if (props.status === gamestatus.redwin) {
            content = <div classname="win red">红方胜利</div>
        } else if (props.status === gamestatus.blackwin) {
            content = <div classname="win black">黑方胜利</div>
        } else {
            content = <div classname="win equal">平局</div>
        }
    }
    return (
        <div classname="status">
            {content}
        </div>
    )
}

export default gamestatuscomp;
.status {
  width: 150px;
  .next,.win{
    font-size: 18px;
  }
  .win{
    border: 2px solid;
    border-radius: 5px;
    width: 100%;
    padding: 10px 0;
  }
  .equal{
    background-color: antiquewhite;
  }
  .red{
    color: #ff4400;
  }
  .black{
    color: #282c34;
  }
}

收尾

最后在app.tsx里调用game组件

import react from 'react';
import './app.scss';
import game from "./component/game";

class app extends react.component{
    render(): react.reactelement<any, string | react.jsxelementconstructor<any>> | string | number | {} | react.reactnodearray | react.reactportal | boolean | null | undefined {
        return (
            <div classname="app">
                <game/>
            </div>
        );
    }
};

export default app;