“我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛”
代码仓库,走过路过点一个 Star ✨
正确的移动这些棋子
灵活的使用轮子
子曾经曰过:有轮子不用谓之小可爱。
这里的就用到了一个挺不错的第三方库 Chess.js
并不是非要每一行代码都是需要自己写出来的,有现成的好用的第三方库用起来不需要有心理负担,毕竟不能所有程序都从0101开始敲起来,我们每个人都站在了巨人的肩膀之上,就别跳下去了吧。
在使用轮子之前,你需要判断你的目的是什么?我是为了学习还是为了完成这个任务。好吧,我是为了完成这个小游戏。说实话,很多 Edge Case 我都没有听说过,有可能一开始就写了一个错误的判断然后越走越远。
记录棋谱
Algebraic Notation 代数记法
// 1. 代表是第一回合,白方先行,把兵拱到了 e4 这个格子,黑方把兵拱到了 e5 这个格子
1.e4 e5
// 第二个回合,白方跳马走到 c3 这个格子,黑方拱兵到 d6
2.Nc3 d6
| 常见符号 | 代表的意思 | 举个例子 |
|---|---|---|
| + | Check 将军 | Ra8+ |
| # | Checkmate 将死 | Qxf7# |
| x | Take 吃 | R7xf5 |
| O-O | kingside castling 短易位 | |
| O-O-O | queenside 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原则了,写起来也很脏。