この記事は新歓ブログリレー2024の46日目の記事です。
こんにちは、21B SysAd班のmehm8128です。
今回はReactで将棋を作った話をします。
僕は高校で将棋部に入っていて、他にやりたい人がいなかったのでなぜか部長もやっていました。将棋ウォーズは原始棒銀だけで初段までいってたのですが、最近は角換わりや右四間飛車を勉強してたまに指しています。
そこで今回、Reactで将棋を作ってみました。
作ったものの紹介をしてから、技術的な話をしていきます。
作ったもの
こちらです。見た目はまだ適当です。
https://shogi-mehm8128.vercel.app/
僕は普段フロントエンド書いてるのと、ゲームだとWebSocketとか使わないといけなくてめんどくさいので、今回はオフラインでのみ対戦できるようになっています。
駒の動きや詰み判定、二歩判定など基本的なルールは実装してありますが、千日手や入玉などあんまり発生しないようなルールはスルーしてます。あと制限時間も実装していません。本当は詰み判定もスルーしようと思ったのですが、なんとか実装できそうだったのでしました(できてないけど)。
僕は自分のPCにShogiGUI+水匠で棋譜解析できる環境を入れてあるのですが、今回作った将棋アプリの棋譜も解析できるように、棋譜データを出力するようにしました。kif形式のかなり簡素なものを出力しています。
また、オフラインで対戦するような友達がいないという人のために、「ぼっちモード」があります。左上のボタンを押してぼっちモードをオンにすると、一手指す毎に盤面を180度回転してくれるので、一人二役でもわざわざPCを回転しなくても常に手番側の目線から指すことができます。
技術的な話
リポジトリはこちら。
https://github.com/mehm8128/shogi
Reactと書いてるのですが、厳密にはテンプレート作ってあるNext.jsで作りました。どっちでもあんまり変わらないです。CSSはKuma-UI、状態管理はjotai、テストはvitestを使いました。特に今回はロジックが多いので、単体テストを頑張って書きました。jotaiにロジックを持たせちゃっている部分もあって、そこはテスト書けていないので(jotaiの使い方も含めて)今後の改善点だと思います。
Kuma-UIでButtonコンポーネントを使ったときにcursor: default
が上手く上書きできなかったり背景色が上手くつかないことがあったり、Biomeで謎のタイミングでif文が消されたりしたのがバグっぽい動作だったので報告したいと思いつつ、小さい構成で再現ができないのでできたら報告しようと思ってます。
駒の動き
駒の動きは各駒を表すファイルで管理しています。例えば飛車は以下のファイルです。
https://github.com/mehm8128/shogi/blob/main/src/features/piece/pieces/rook.ts
canMoveNotPromotedRook
は成っていない状態の飛車の動きで、今の飛車の位置、所有しているプレイヤー、盤面の状態を引数に取り、動けるマスの配列を返します。canMoveToList
に動ける候補のマスを入れ、その中から盤外にあるマスや、他の駒にぶつかってそれ以上先に進めないマスを除外しています。
canMoveNotPromotedRook
の下には竜王の動きを書いたcanMovePromotedRook
も定義しています。
テストはこちらです。
https://github.com/mehm8128/shogi/blob/main/src/features/piece/pieces/rook.test.ts
先手と後手、成と不成で1つずつ書いていて、どの駒も4四にいるときのテストを書いています。
詰み判定
実はまだ途中です。
https://github.com/mehm8128/shogi/blob/main/src/features/game/checkmate.ts
途中まで作って残りは別の日にしたらモチベがなくなったので、詰んでるのに詰まない判定になったり詰んでいないのに詰んでる判定になったりします。一応2パターンだけテスト↓で確認済みです。
https://github.com/mehm8128/shogi/blob/main/src/features/game/checkmate.test.ts
処理が多すぎてパフォーマンス的にかなりよくなさそうなのですが、どう削ればいいかも分からずという感じです。いい方法あれば誰か教えてください。
kif
kif出力の処理についてです。
各手のhistoryをjotaiのatomで持っていて、以下のようになっています(コメントは今回説明のためにつけました)。移動元のマスはkif形式では入れないといけなくて、同じマスに同じ種類の駒が左からも右からも移動できるときに、「左」とか「右」とかつけるのが大変なので出力するときは楽にして、入力するときに棋譜解析ソフト側に頑張ってもらおうって感じなんですかね。
export interface History {
before: Coordinate | null // 移動元のマス
after: Coordinate // 移動先のマス
pieceType: HistoryPieceType // 駒の種類を日本語にしたもの
decorator: Decorator // 成or打or空文字
}
export const historiesAtom = atom<History[]>([])
kif出力の関数は以下にあります。
https://github.com/mehm8128/shogi/blob/main/src/features/game/kif.ts
テストもあります。
https://github.com/mehm8128/shogi/blob/main/src/features/game/kif.test.ts
おまけ
駒落ちはinitBoard
で駒を消すと任意を駒を落とせます。
https://github.com/mehm8128/shogi/blob/main/src/features/game/const.ts
まとめ
将棋をやりましょう。ぼっちでもできます。
最近はチェスも勉強したいなーって思ったり思わなかったりしてます。
明日の担当はcpくんです、お楽しみにー