"在這裡閱讀效果更佳" 還記得當年和同桌在草稿紙上下三子棋的時光嗎 今天我們就用代碼來重溫一下年少(假設你有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
刪掉初始化界面的一些代碼
開發階段
組件化
開發一個項目其實就是開發組件
把一個項目拆分一個個小組件,方便後期維護以及復用
- 棋子組件
- 棋盤組件
- 游戲規則組件
- 游戲狀態組件
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;