♟️

Flutter で将棋ゲームを作ろう #2 駒を配置しよう

2023/06/27に公開

はじめに

こんにちは!水瀬ひろと申します。
趣味で Flutter を楽しんでいるエンジニアです。

僕はソシャゲ(特にHoYoverse作品)が好きで、自分でもゲームを作ってみたいと思い、まずは将棋ゲームに挑戦してみました。
Flame などのライブラリは使用せず、自前実装のみでおこなったので学びが多かったです。

将棋ゲームの制作を通じて、以下のようなことを学ぶことができたので、シリーズ化して残しておこうと思います。

将棋ゲームの作り方やルールの理解
応用としてボードゲーム系アプリの作成手法の習得
ゲームの基本的な動作原理についての理解
このシリーズでは、実際に行った実装内容を細かく解説していきます。
最終的には以下のような将棋ゲームが完成する予定です!
将棋ゲーム

駒のクラスを作成

前回は将棋盤を表示できるようにしました。
https://zenn.dev/flutteruniv_dev/articles/9c0ea026c6393d

今回は駒を配置していきます。
まずは shogi_piece.dart というファイルを作成し、
駒の種類をあらわすenumと駒のクラスを用意します。

piece.dart
// 駒の種類
enum ShogiPieceType {
  ousho,
  gyokusho,
  hisya,
  promotedHisya,
  kakugyo,
  promotedKakugyo,
  kinsho,
  ginsho,
  promotedGinsho,
  keima,
  promotedKeima,
  kyousya,
  promotedKyousya,
  hohei,
  promotedHohei,
}

class ShogiPiece {
  final ShogiPieceType type; // 駒の種類
  final bool isAlly; // 味方の駒かどうか
  final String imagePath; // 駒の画像
  final bool isPromoted; // 成っているかどうか

  ShogiPiece({
    required this.type,
    required this.isAlly,
    required this.imagePath,
    required this.isPromoted,
  });
}

ひとつひとつのマスとなるウィジェットを作成

次に square.dart というファイルを作成します。
ここで定義した Square を81個並べて、将棋盤を表示するように後ほど変更します。

square.dart
import 'package:flutter/material.dart';
import 'package:for_zenn/shogi_piece.dart';

class Square extends StatelessWidget {
  final ShogiPiece? shogiPiece;

  const Square({
    super.key,
    required this.shogiPiece,
  });

  
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.orange[100], // 背景を薄めのオレンジ色に
        border: Border.all(color: Colors.black, width: 1.0),
      ),
      // 駒を引数に受け取っていれば駒の画像を表示
      child: shogiPiece != null ? Padding(
        padding: const EdgeInsets.all(5.0),
        child: Image.asset(shogiPiece!.imagePath),
      ) : null,
    );
  }
}

駒の画像を入手

今回はこちらのサイトからいただきました。
将棋駒一覧
https://sunfish-shogi.github.io/shogi-images/#hitomoji

名前を変えつつ lib/images ディレクトリに入れていきます。
lib/images ディレクトリ

以下のような命名にしました。
自陣用の駒画像を up_
敵陣用の駒画像を down_
成った後の画像は promoted_

画像を有効化するために pubspec.yaml も編集しましょう。

pubspec.yaml
flutter:
  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  assets:
    - lib/images/

歩の駒を配置する

これで準備が整ったので、まずは「歩」から駒を配置していきます!
前回作成した将棋盤の初期化メソッド内でおこないます。

main.dart
// 盤面の初期化
void initializeBoard() {
  List<List<dynamic>> newBoard = List.generate(9, (index) => List.generate(9, (index) => null)); // 9 × 9
  
  // いずれここで駒の初期配置などをおこなう

  shogiBoard = newBoard;
}

まず、 newBoard は以下のような形で、GridView で表示する将棋盤と対応するようになっています。

自陣の「歩」は上から7番目の列に並ぶので
左上から座標のように数えると[6,0], [6,1], [6,2]...と並べれば良さそうです。
敵陣も同じように[2,0], [2,1], [2,2]...となりますね。
書いてみましょう。

main.dart
// 盤面の初期化
void _initializeBoard() {
  List<List<ShogiPiece?>> newBoard = List.generate(9, (index) => List.generate(9, (index) => null));

  // 駒の初期配置
  // 歩を配置
  for (int i = 0; i < 9; i++) {
    // 敵陣
    newBoard[2][i] = ShogiPiece(
      type: ShogiPieceType.hohei, // 駒の種類は歩
      isAlly: false, // 味方の駒ではない
      imagePath: "lib/images/down_hohei.png", // 下向きの駒画像
      isPromoted: false, // 成っていない
    );

    // 自陣
    newBoard[6][i] = ShogiPiece(
      type: ShogiPieceType.hohei, // 駒の種類は歩
      isAlly: true, // 味方の駒
      imagePath: "lib/images/up_hohei.png", // 上向きの駒画像
      isPromoted: false, // 成っていない
    );
  }

  board = newBoard;
}

これで shogiBoard の2,6行目には ShogiPiece が入っているはずなので、GridView で表示させます。

main.dart

Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        GridView.builder(
          shrinkWrap: true, // GridView のサイズを itemBuilder で表示するコンテンツによって決定
          itemCount: 9 * 9,
          physics: const NeverScrollableScrollPhysics(), // スクロールさせない
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 9), // 9列で表示
          itemBuilder: (context, index) {
            int row = index ~/ 9; // 縦軸
            int col = index % 9; // 横軸

            return Square(
              shogiPiece: shogiBoard[row][col],
            );
          },
        ),
      ],
    )
  );
}

新しい部分は以下。

int row = index ~/ 9; // 縦軸
int col = index % 9; // 横軸

こちらは index から縦軸・横軸の座標を割り出しています。

例えば index が 56 の時。
56 / 9 = 6 あまり 2
なので
row: 6
col: 2

56番目の位置を正しく指定できていそうです。
座標

もう一点はこちら。

return Square(
  shogiPiece: shogiBoard[row][col],
);

もともと表示していた Scaffold の代わりに、
square.dart に定義した Square を表示しつつ、引数に shogiBoard の対応する座標の中身を渡しています。
shogiBoard の2,6行目には ShogiPiece が入っているはずなので、 Square にうまく渡っていればコマも表示されるはずです。

できましたね!
iOS シミュレータ

他の駒を配置する

あとは歩と同じように shogiBoard に設定していきましょう!

main.dart
// 盤面の初期化
void initializeBoard() {
  List<List<dynamic>> newBoard = List.generate(9, (index) => List.generate(9, (index) => null)); // 9 × 9

  // 敵陣
  newBoard[0][0] = ShogiPiece(
    type: ShogiPieceType.kyousya,
    isAlly: false,
    imagePath: "lib/images/down_kyousya.png",
    isPromoted: false,
  );
  newBoard[0][1] = ShogiPiece(
    type: ShogiPieceType.keima,
    isAlly: false,
    imagePath: "lib/images/down_keima.png",
    isPromoted: false,
  );
  newBoard[0][2] = ShogiPiece(
    type: ShogiPieceType.ginsho,
    isAlly: false,
    imagePath: "lib/images/down_ginsho.png",
    isPromoted: false,
  );
  newBoard[0][3] = ShogiPiece(
    type: ShogiPieceType.kinsho,
    isAlly: false,
    imagePath: "lib/images/down_kinsho.png",
    isPromoted: false,
  );
  newBoard[0][4] = ShogiPiece(
    type: ShogiPieceType.ousho,
    isAlly: false,
    imagePath: "lib/images/down_ousho.png",
    isPromoted: false,
  );
  newBoard[0][5] = ShogiPiece(
    type: ShogiPieceType.kinsho,
    isAlly: false,
    imagePath: "lib/images/down_kinsho.png",
    isPromoted: false,
  );
  newBoard[0][6] = ShogiPiece(
    type: ShogiPieceType.ginsho,
    isAlly: false,
    imagePath: "lib/images/down_ginsho.png",
    isPromoted: false,
  );
  newBoard[0][7] = ShogiPiece(
    type: ShogiPieceType.keima,
    isAlly: false,
    imagePath: "lib/images/down_keima.png",
    isPromoted: false,
  );
  newBoard[0][8] = ShogiPiece(
    type: ShogiPieceType.kyousya,
    isAlly: false,
    imagePath: "lib/images/down_kyousya.png",
    isPromoted: false,
  );
  newBoard[1][1] = ShogiPiece(
    type: ShogiPieceType.hisya,
    isAlly: false,
    imagePath: "lib/images/down_hisya.png",
    isPromoted: false,
  );
  newBoard[1][7] = ShogiPiece(
    type: ShogiPieceType.kakugyo,
    isAlly: false,
    imagePath: "lib/images/down_kakugyo.png",
    isPromoted: false,
  );

  // 自陣
  newBoard[7][1] = ShogiPiece(
    type: ShogiPieceType.kakugyo,
    isAlly: true,
    imagePath: "lib/images/up_kakugyo.png",
    isPromoted: false,
  );
  newBoard[7][7] = ShogiPiece(
    type: ShogiPieceType.hisya,
    isAlly: true,
    imagePath: "lib/images/up_hisya.png",
    isPromoted: false,
  );
  newBoard[8][0] = ShogiPiece(
    type: ShogiPieceType.kyousya,
    isAlly: true,
    imagePath: "lib/images/up_kyousya.png",
    isPromoted: false,
  );
  newBoard[8][1] = ShogiPiece(
    type: ShogiPieceType.keima,
    isAlly: true,
    imagePath: "lib/images/up_keima.png",
    isPromoted: false,
  );
  newBoard[8][2] = ShogiPiece(
    type: ShogiPieceType.ginsho,
    isAlly: true,
    imagePath: "lib/images/up_ginsho.png",
    isPromoted: false,
  );
  newBoard[8][3] = ShogiPiece(
    type: ShogiPieceType.kinsho,
    isAlly: true,
    imagePath: "lib/images//up_kinsho.png",
    isPromoted: false,
  );
  newBoard[8][4] = ShogiPiece(
    type: ShogiPieceType.gyokusho,
    isAlly: true,
    imagePath: "lib/images/up_gyokusho.png",
    isPromoted: false,
  );
  newBoard[8][5] = ShogiPiece(
    type: ShogiPieceType.kinsho,
    isAlly: true,
    imagePath: "lib/images/up_kinsho.png",
    isPromoted: false,
  );
  newBoard[8][6] = ShogiPiece(
    type: ShogiPieceType.ginsho,
    isAlly: true,
    imagePath: "lib/images/up_ginsho.png",
    isPromoted: false,
  );
  newBoard[8][7] = ShogiPiece(
    type: ShogiPieceType.keima,
    isAlly: true,
    imagePath: "lib/images/up_keima.png",
    isPromoted: false,
  );
  newBoard[8][8] = ShogiPiece(
    type: ShogiPieceType.kyousya,
    isAlly: true,
    imagePath: "lib/images/up_kyousya.png",
    isPromoted: false,
  );

  // 歩を配置
  for (int i = 0; i < 9; i++) {
    newBoard[2][i] = ShogiPiece(
      type: ShogiPieceType.hohei,
      isAlly: false,
      imagePath: "lib/images/down_hohei.png",
      isPromoted: false,
    );

    newBoard[6][i] = ShogiPiece(
      type: ShogiPieceType.hohei,
      isAlly: true,
      imagePath: "lib/images/up_hohei.png",
      isPromoted: false,
    );
  }

  shogiBoard = newBoard;
}

かなりベタ書きなので効率化できそうな気もしますが、とりあえずこれで全てのコマを配置できました!!

iOS シミュレータ

終わりに

お疲れさまでした〜!
実際に駒を配置するとそれだけで将棋ゲームが完成したような雰囲気がありますね。

質問などあれば気軽にコメントください!
次回は駒を動かせるようにしていきます。
https://zenn.dev/flutteruniv_dev/articles/d84f5b822dba21

この後も以下のような記事を準備しているのでお付き合いいただけると嬉しいです。

  • 相手の駒を取る
  • 「成り」の実装
  • 持ち駒を盤面に出す
  • 王手の判定
  • 詰みの判定
  • CPUを実装する
  • 「二歩」の禁止
  • 棋譜を記録する

Twitter

https://twitter.com/minase_hiro_

Threads

https://www.threads.net/@minasehiro

Flutter大学

Discussion