React + Ts 实现三子棋小游戏
还记得当年和同桌在草稿纸上下三子棋的时光吗
今天我们就用代码来重温一下年少(假设你有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
删掉初始化界面的一些代码
开发阶段
组件化
开发一个项目其实就是开发组件
把一个项目拆分一个个小组件,方便后期维护以及复用
- 棋子组件
- 棋盘组件
- 游戏规则组件
- 游戏状态组件
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文件夹下
棋子组件我们使用函数组件,思考需要传入组件的属性的类型:
- type(棋子的类型)
- 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
棋盘组件我们需要传递三个参数:
- 棋子的数组
- 游戏是否结束
- 点击事件函数
循环数组渲染棋子, 并给游戏是否结束一个默认值
全部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;