如何写一个国际象棋的游戏(第六部分)CSS伪元素的另一种使用方法|更加高效的使用 React Context

1,485 阅读5分钟

“我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

代码仓库,走过路过点一个 Star ✨

正确的移动这些棋子

灵活的使用轮子

子曾经曰过:有轮子不用谓之小可爱。

这里的就用到了一个挺不错的第三方库 Chess.js

并不是非要每一行代码都是需要自己写出来的,有现成的好用的第三方库用起来不需要有心理负担,毕竟不能所有程序都从0101开始敲起来,我们每个人都站在了巨人的肩膀之上,就别跳下去了吧。

在使用轮子之前,你需要判断你的目的是什么?我是为了学习还是为了完成这个任务。好吧,我是为了完成这个小游戏。说实话,很多 Edge Case 我都没有听说过,有可能一开始就写了一个错误的判断然后越走越远。

记录棋谱

Algebraic Notation 代数记法

// 1. 代表是第一回合,白方先行,把兵拱到了 e4 这个格子,黑方把兵拱到了 e5 这个格子
1.e4 e5

image-20220331215941266.png

// 第二个回合,白方跳马走到 c3 这个格子,黑方拱兵到 d6
2.Nc3 d6
常见符号代表的意思举个例子
+Check 将军Ra8+
#Checkmate 将死Qxf7#
xTake 吃R7xf5
O-Okingside castling 短易位
O-O-Oqueenside castling 长易位

基本上记住上面的符号,你就可以记录一整个棋局了。

⚠️ 这里需要注意的是棋子的名称需要大写,比如车 R、马 N、象 B 等,而走到的格子坐标是小写的。

Forsyth-Edwards Notation FEN记法

FEN 记法是对棋盘某一时刻上面的一个记录,这个记法包含了足够的信息用来重新开始游戏,可以理解为游戏里面的存档。

// 这是起始棋局的格式
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
// 1.e4
rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1
// 1.c5
rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2

在代码中的使用

import React, { useContext } from 'react';
import { BoardContext } from 'src/context/board';
import chunk from 'lodash/chunk';
import djs from 'dayjs';
import { toPiece } from 'src/operations';
​
const ChessManual = () => {
    const { chessboard } = useContext(BoardContext);
    const history: any[] = chunk(chessboard.history({ verbose: true }), 2);
    return <article className='article'>
        <h4 className=''>Chess Manual</h4>
        <p className="article-meta">Created At {djs().format('YYYY-MM-DD')}</p>
        <div className="row">
            <div className="col-12 padding-none">
                <table>
                    <thead>
                        <tr>
                            <th>#</th>
                            <th>Player 1</th>
                            <th>Player 2</th>
                            <th>Notation</th>
                        </tr>
                    </thead>
                    <tbody>
                        {history.map(([first, second], idx: number) => {
                            return <tr key={idx}>
                                <td>{idx + 1}</td>
                                <td>{first ? `${toPiece({ color: first.color, type: first.piece })} from ${first.from} to ${first.to}` : ``}</td>
                                <td>{second ? `${toPiece({ color: second.color, type: second.piece })} from ${second.from} to ${second.to}` : ``}</td>
                                <td>{first?.san} {second?.san}</td>
                            </tr>
                        })}
                    </tbody>
                </table>
            </div>
​
            <div className="col-12">
                <pre>
                    <code>
                        {chessboard.fen()}
                    </code>
                </pre>
            </div>
        </div>
​
    </article>
};
​
export default ChessManual;

其中 chessboard.history()方法会返回每一步的信息,但是我们想要在表格的一行显示。先说一下我的思路,表格的一行有黑白方的记录,而这个方法只是每次返回一个,所以我需要把这个数组每两个拆分成一个小数组,这样我只需要一遍循环就可以显示出来了。千万不要蠢蠢的去写两遍循环,或者单独写一个方法去处理这个数组。 学会回到第一个部分,灵活的使用轮子!用 Lodash 这里包含了你能想到或者想不到的常用的数组操作方法。

这里使用到了 import chunk from 'lodash/chunk'; 这个方法:

_.chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]
 
_.chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]// 那么在这里我就可以这样处理这个 history 数组。
const history: any[] = chunk(chessboard.history({ verbose: true }), 2);
history.map(([first, second], idx: number) => {});

ES6 里面其实也内置了很多常用数组操作的办法比如 map filter reduce 这些,使用好它们能减少很多for循环的操作,因为我本身是函数式的爱好者,而且我觉得循环写起来太上头,我觉得代码需要一个更加可以被理解的方式去表达出来,这是关键。在 Clojure 里面也有很多 core 方法是有这些内置的操作的,在 Clojure 里面叫做 partition

;; partition a list of 20 items into 5 (20/4) lists of 4 items
(partition 4 (range 20))
;;=> ((0 1 2 3) (4 5 6 7) (8 9 10 11) (12 13 14 15) (16 17 18 19))
​
;; partition a list of 22 items into 5 (20/4) lists of 4 items 
;; the last two items do not make a complete partition and are dropped.
(partition 4 (range 22))
;;=> ((0 1 2 3) (4 5 6 7) (8 9 10 11) (12 13 14 15) (16 17 18 19))

伪元素的另一种用法

提示轮到哪一方

<div className="flex-center align-middle">
  <div className="col">
    Current Move: {chessboard.turn() === 'w' ? 'White' : 'Black'}
  </div>
</div>

这一段没有很复杂的逻辑,因为 turn这个方法是内置的。

提示当前选择的棋子可以走到哪一些格子

<fieldset className="form-group">
  <label htmlFor="showTips" className="paper-switch-label">
    Show Availble Moves
  </label>
  <label className="paper-switch">
    <input id="showTips" name="showTips" type="checkbox" checked={showTips} onChange={() => setShowTips(s => !s)} />
    <span className="paper-switch-slider round"></span>
  </label>
</fieldset>

有趣的这里,PaperCSS 里面使用了大量的 <input checked /> 来表达不同状态下面的样式,checked 是一种样式,没有 checked 是另外一种样式,而这个过程完全不需要 js 参与进来,完全可以通过样式来表达。

.tabs {
  .content {
    display: none;
    flex-basis: 100%;
    padding: 0.75rem 0 0;
  }

  input {
    display: none;

    &:checked + label {
      @include color(color, 'primary');
      @include color('border-bottom-color', 'secondary');
      border-bottom-style: solid;
      border-bottom-width: 3px;
    }

    @for $num from 1 through 5 {
      &[id$='tab#{$num}']:checked~div[id$='content#{$num}'] {
        display: block;
      }
    }
  }

  label {
    @include color('color', primary-light);
    display: inline-block;
    font-weight: 600;
    margin: 0 0 -1px;
    padding: 0.75rem;
    text-align: center;

    &:hover {
      @include color('color', muted);
      cursor: pointer;
    }
}

通过 &:checked这个 Pseudo-classes 来控制样式是一个非常得体的方法。在 PaperCSS 里面 Slider、Collapse 这些组件都通过这样的方式来实现。有兴趣的话可以看一下 PaperCSS 源代码

游戏结束的提示

// Alert Context
import React, { FC, createContext, useState } from 'react';
interface AlertContextProps {
    show: boolean;
    text: string;
    toast: (str: string) => void;
    clear: () => void;
}
export const AlertContext = createContext<AlertContextProps>({} as AlertContextProps);
​
const AlertContenxtProvider: FC = ({ children }) => {
    const [show, setShow] = useState(false);
    const [text, setText] = useState('Hello Chess!');
    return <AlertContext.Provider
        value={{
            show,
            text,
            toast: (str: string) => {
                setText(str);
                setShow(true);
            },
            clear: () =>{
                setText('');
                setShow(false);
            },
        }}
    >
        {children}
    </AlertContext.Provider>
​
};
export default AlertContenxtProvider;
// Alert 组件
import React from 'react';
import { AlertContext } from 'src/context/alert/index';
import './style.scss';
​
const Alert = () => {
    const { text, clear, show } = React.useContext(AlertContext);
    return show ? <>
        <div className="col fixed-alert" onClick={clear}>
            <span className="alert alert-primary">
                {text}
            </span>
        </div>
    </> : <></>
}
​
export default Alert;

这里通过 Provider 这个组件,其实我可以把 useEffect等 hooks 放在这个组件里面去写,多做一层封装包括初始化数据之类。也可以在 Context 中返回方法,方便我的组件可以直接调用,而不是仅仅把 Context 只是作为一个 state 存储器来用,然后在别的地方去 setState 更新这个 Context,这样就违反了SOLID原则了,写起来也很脏。