网页版扫雷 -- React练习

开会不准带电脑,手机app玩扫雷玩到眼快瞎,而且每次都要忍受长达十秒的广告,自己写一个算了。
详细代码在git里,还在更新。

第一步: React & webpack setup

用webpack主要目的是为了搭建一个简单的webpack server, 顺带着直接用它打包发布好了.

set up React

React需要安装一系列相关包,什么ReactDom啊之类的,直接 npm install XX --save-dev安装之就好。

set up webpack

npm install webpack --save-dev
npm install webpack-dev-server --save-dev
npm install html-webpack-plugin --save-dev

--save-dev是因为这些包只为开发用,不是给user用的
然后配置一下webpack.config.js:

    entry: { //程序的入口,类似于定义java的main方法,逻辑从这里开始
        app: path.resolve(__dirname, 'src') + '/index.js'
    },
    devServer: {//定义webpack-server
        port: 8089, // localhost:8089/会访问到本地的页面
        contentBase: [
            path.join(__dirname, 'site/'), // localhost:8089/index.html会访问到server的site/index.html
        ],
    },
    output: { //所有的js代码都输出到bundle.js里,然后放到site/文件夹里
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'site/')
    },
    resolve: { 
        modules: ['node_modules'],
        extensions: ['.js', '.jsx'], //import xx.js或者xx.jsx可以省略后缀
    },
    plugins: [ // 因为有一个html template,所以需要html-webpack-plugin来添加html文件。
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: 'src/index.html'//html template直接放在src根目录里。
        }),
    ],

然后在package.json里面的
scripts里面添加:

 "build": "webpack -d",
 "start:dev": "webpack -d && webpack-dev-server --progress --colors --config ./webpack.config.js"

这样就可以用 npm run build来打包所有代码到site里
npm run start:dev来启动本地的webpack server

webpack要点大概就这些了。配置好了之后就开始写代码了。

第二步: 生成一个布好雷的方格:

数据结构:

扫雷的数据结构没什么可以纠结的,一个二维数组。

数据类型:

也没啥好纠结的,数字嘛。
数字范围就是1-8,也没啥悬念嘛。 表示周围有1-8个雷(玩了几千把扫雷,只遇到过一次8)。
空白的格子就用0表示了,表示周围没有雷嘛。
还剩下一个9就用来表示雷好雷嘛。到此为止,一位数的数字全部用上了。

生成数组:

剩下的问题就是,怎么把这个数组生成出来。
首先,根据我几千把扫雷的经验,这应该是个随机生成的数组。不大可能有什么库之类的。
那么如何随机生成?
每一盘,雷的个数是不变的,那也就是说,我们可以随机找几个为止把雷放进去,然后计算剩下的每个格子周围有多少个雷就可以了。
事情就简单起来了:(以简单为例子,10*8的矩阵,10个雷。)

  1. 生成一个10*8长度的数组。塞进去10个9.(怎么喜欢怎么来)
    - 可以先放进去10个9,70个0,然后shuffle 打乱顺序
    - 也可以先生成一个80个0 的数组,然后随机抽取10个位置替换为9.
  2. 把这个数组每8个一截,折断成10截,放进一个二维数组。
  3. 开始计算这个二维数组所有目前为0的位置周围有多少个9,然后替换成真正的数字。

一个由0-9组成的二维数组就好了。注意数雷的时候小心处理边界条件。

第三步:template和js入口

html

react的话 html 的template就很简单了。只有几行:


<html>
<head>
    <meta charset="utf-8">
    <meta content="width=device-width, initial-scale=1" name="viewport" />
    <title>saoleititle>
head>
<body>
<div id="game-wrapper"/>
body>
html>

第二行 meta是为了以后调mobile用的css用的
实际上html的本体只有一行,game-wrapper。 不用提react的事,也不用提css,也不用提scripts。

react入口:

上面的webpack说好了,入口要从src/index.js开始, 所以这个文件就是入口了,也很简单:

import React from 'react';
import ReactDOM from 'react-dom'
import Game from './components/Game';

ReactDOM.render(<Game />, document.getElementById('game-wrapper'));

将id为game-wrapper的元素替换为Game component. Game是从 ./components/Game里import的,但是不用提html的事情。 这两个会在webpack打包的时候结合为一体。
打包之后的html: site/index.html

<body>
	<div id="game-wrapper"/>
	<script type="text/javascript" src="bundle.js">script>
body>

第四步: Components:

请不要吐槽变量名字

最顶层的Components是Game
然后看,Game要包含啥
扫雷嘛,就一个MN的格子加上几个按钮(开始,难度),所以需要两种Component: GameBoardButton
最后:
GameBoard其实是有M
N个小的格子组成,所以GameBoard需要一种Component: SingleGrid
所以components包如图:
网页版扫雷 -- React练习_第1张图片
在下面解释中,GameGameBoardButton的爸爸,GameBoardSingleGrid的爸爸。

Game.jsx:

作为最顶层的Game,

  1. 它要知道自己的状态(在玩着还是已经结束(赢或者输))记录在status
  2. 需要知道自己的难度(多少行,多少列,多少雷)
import React from 'react';
import GameBoard from './GameBoard';
import Button from './Button';
import '../styles/index.css';

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            totalRows: 10,
            totalColumns: 8,
            totalLei: 10,
            status: 'inProgress',
            gameIndex: 0,
        };
        //function bind this省略
    }

    gameFinish() {/*游戏结束时触发这个方法*/}
    /*button们的方法在这里*/
    setEasyGame() {/*省略*/}
    setMediateGame() {/*省略*/}
    setHardGame() {/*省略*/}
    reStart() {/*省略*/}
    render () {
        return (
            <React.Fragment>
                <div className={`gameboard${this.state.totalColumns}`}>
                    <GameBoard key={this.state.gameIndex} {/*把方法和属性传给儿子GameBoard*/}/>
                </div>
                <div className={`buttons${this.state.totalColumns}`}>
                    <Button content="New Game" color="green"  onClick={this.reStart}/>
                    <Button content="Easy" color="green" onClick={this.setEasyGame}/>
                    //还有俩button
                </div>
            </React.Fragment>
        )
    }
}

export default Game

GameBoard.jsx:

作为大格子,需要知道它爸爸定义了的游戏等级,自己有多少行多少列(从Game那里得到)

  1. 需要知道每个格子都是什么数值。 gameMatrix。 每个新的游戏都要generate一个新的矩阵。
  2. 需要知道游戏进度 游戏失败或者成功都是从一次鼠标点击小格子的事件触发的
    - fail: 左键点击到了一个雷。
    - win: 左键点击到了最后一个还没打开的数字。
    - 都不是上面两种情况,则是还在进行中。

除此之外,还需要知道哪些格子被打开了(如果点到空白的格子,需要检查周围的格子)。所以,在这一层维护了一个opened数组,用来记录被打开了的格子,和方便计算成功或者失败。

import React from 'react';
import SingleGrid from './SingleGrid';
import {generateGame} from "../service/dataService";
import '../styles/grid.css'

class GameBoard extends React.Component {
    constructor(props) {
        super(props);
        const {totalRows, totalColumns, totalLei} = props;
        this.state = {
            gameMatrix:  generateGame(totalRows, totalColumns, totalLei),
            opened: [],
        };
        //方法绑定this
    }

    gameFail(status) {/*被儿子调用,然后调用爸爸的gameFail方法*/};
    openGrid(position) {/*被儿子调用然后计算是不是最后一个数字格子,如果是,调用爸爸的gameWin方法*/};
    handleZero(position, totalRows, totalColumns){/*如果被儿子通知,点到空白的格子了,*/
   												 /*则需要检查四周格子,把所有空白格子都打开*/}
    render () {
        const { totalRows, totalColumns, gameStatus } = this.props;
        return (
            <ul> // 用两个map加载小格子们
                {this.state.gameMatrix.map((row, rowNumber)=>
                    (<p className="row" key={ rowNumber }>{row.map(
                        (grid, gridNumber) =>
                            <SingleGrid value={grid} {/*其他需要传给小格子的属性*/}>
                            </SingleGrid>
                    )}</p>)
                )}
            </ul>
        )
    }
}
export default GameBoard

SingleGrid.jsx:

作为最小的单元,也是和user直接交互的component,需要知道:

  1. 自己的值是什么(数字或者雷或者空白)
  2. 自己是被打开了还是被标记成雷了
  3. 如果被打开了,是被用户点击打开的还是被爸爸通知打开的(检查空白格子产生情况)
  4. 另外,还需要知道整个游戏的状态: 如果赢了或者输了,则所有的格子都不能被点击
    如果输了,还需要判断被标记的状态和实际状态是不是相符,如果不是需要给出错误判断:
    网页版扫雷 -- React练习_第2张图片
/**
 * Created by xzou2 on 11/13/18.
 */
import React from 'react';
import '../styles/grid.css'

class SingleGrid extends React.Component {
    constructor(props) {
        super(props);
        this.state = {status: 'default', opened: false};
        //方法绑定this
    }

    handleClick() {/*左键点击,需要根据当前自己的状态来判断状态变化*/}
    handleRightClick(e) {/*右键点击,设置为小红旗或者是取消小红旗*/}
    handleTouchEnd(){/*略*/}
    touchStart(){/*给手机等触屏玩儿的*/}
    componentWillReceiveProps(nextProps) {/*检查从爸爸那里得来的opened属性,*/
    										/*如果是爸爸让自己打开,而自己还没有打开,那么改变状态*/}
    render () {
        const { value, gameStatus } = this.props;
        let currentStatue = '';
        if (gameStatus !== 'fail') {
         //判断每个格子需要显示的东西
        } else {
         //游戏已经输了判断每个格子的
        }
        return <span
            className={`grid${currentStatue} grid ${currentStatue && currentStatue!== 'marked' ? 'open' : 'default' }`}
            onClick={this.handleClick}
            onContextMenu={this.handleRightClick}
            onTouchStart={this.touchStart}
            onTouchEnd={this.handleTouchEnd}
        >
            {/[1-8]/.test(currentStatue) || currentStatue === 'X' ? currentStatue: ''}</span>
    }
}
export default SingleGrid

Button.jsx:

一个简单的component

class Button extends React.Component {
    render() {
        const {
            color,
            content,
            onClick,
        } = this.props;

        return (
            <button className={color} onClick={onClick}>
                {content}
            </button>
        )
    }
}
export default Button

第五步: 处理用户点击事件:

这大概是最好玩儿最头疼的一个步骤了。逻辑比较复杂,如果需要,请看源码
在最开始的时候,所有的小格子都需要藏起来,这时候小格子的状态是没被打开过的。

左键点击

根据上面的component设计,用户点击的是每一个小格子。每个小格子都知道自己的值,用户点击之后:

  • 如果小格子的值是9, 那就是点到雷了,应该直接宣布失败。这时候要一层层的告诉爸爸和爸爸的爸爸,游戏失败了。
  • 如果小格子的值是0,那么就是点到空白地方了,这时候要往外扩散。所以除了改自己的状态之外,还需要告诉爸爸,点到0了,让爸爸去打开自己周围的格子,这时候爸爸需要一个递归,来打开这个格子周围的所有为0的格子以及0外面一圈的格子。这时候,这个格子周围的格子的状态是由爸爸改变的,而不是通过用户点击。
  • 如果小格子的值是1-8,这是最简单的,直接翻开显示数字就行。小格子的状态改为打开。这时候有可能是最后一个还没打开的格子,所以需要告诉爸爸,自己打开了,求检查是不是最后一个,如果是,爸爸则会宣布游戏结束。
  • 如果这个格子已经被打开了,则什么都不能做了
  • 如果这个格子被标记成小红旗了,则改到问号状态,或者是标记成问号状态,则改回到小红旗。

右键点击事件:

小红旗和隐藏切换。
右键点击事件比较抓狂的是,手机不支持右键,这个纠结过程在另一个博客里了

最后,图片

小红旗的图片和雷的图片用css就可以绑定到状态上。className可以根据情况拼一个字符串出来。

最后的最后,戳这里可以直接开会的时候玩儿扫雷了。

你可能感兴趣的:(游戏,javascript,dataStructure)