♟️

Flutter で将棋ゲームを作ろう #3 駒を動かそう(後編)

2023/07/01に公開

はじめに

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

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

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

  • 将棋ゲームの作り方やルールの理解
  • 応用としてボードゲーム系アプリの作成手法の習得
  • ゲームの基本的な動作原理についての理解

このシリーズでは、実際に行った実装内容を細かく解説していきます。
最終的には以下のような将棋ゲームが完成する予定です!
将棋ゲーム

駒を動かそう

前編では駒を選択できるようにして、さらに移動可能な範囲も見えるようにしました。
https://zenn.dev/flutteruniv_dev/articles/d84f5b822dba21

後半ではいよいよ駒を動かせるようにしていきます!
まずは駒を動かすためのメソッドを作りましょう。

main.dart
// 駒を移動
void movePiece(int newRow, int newCol) {
  shogiBoard[newRow][newCol] = selectedPiece; // 新しい座標へ移動
  shogiBoard[selectedRow][selectedCol] = null; //元の座標を初期化
}

とりあえず今はこれだけです。
座標を受け取り、駒を移動させ、元の座標は空にしています。

次は movePiece を呼べるようにしましょう。

main.dart
// ピースを選択する
void selectPiece(int row, int col) {
  // 駒を選択していない状態から駒を選択した時
  if (selectedPiece == null && shogiBoard[row][col] != null) {
    setState(() {
      selectedPiece = shogiBoard[row][col];
      selectedRow = row;
      selectedCol = col;

      validMoves = calculateValidMoves(shogiBoard, selectedRow, selectedCol, selectedPiece); // 移動可能な座標を計算
    });
    // 移動可能な座標を選択した時
  } else if (selectedPiece != null && validMoves.any((coordinate) => coordinate[0] == row && coordinate[1] == col)) {
    movePiece(row, col);
  }
}

駒を動かしたいのは、駒を選択中 かつ 移動可能なマスをタップした時。

selectedPiece != null &&
  validMoves.any((coordinate) => coordinate[0] == row && coordinate[1] == col)

ここで判定しています。

main.dart
// 駒を移動
void movePiece(int newRow, int newCol) {
  shogiBoard[newRow][newCol] = selectedPiece; // 新しい座標へ移動
  shogiBoard[selectedRow][selectedCol] = null; //元の座標を初期化

  // 現在の選択をリセット
  setState(() {
    selectedPiece = null;
    selectedRow = -1;
    selectedCol = -1;
    validMoves = [];
  });
}

そして駒を移動したあとは、選択を解除させたいので selectedPiece などを初期化します。

これで駒の移動が可能になりました🎉
iOS シミュレータ

ターンチェンジを実装しよう

駒を動かせるようになったことでかなり将棋に近づいてきましたね。
最後に、ターンチェンジできるようにして終わりたいと思います。

今のままだと自陣の駒も敵陣の駒も順番も関係なく動かせてしまうので、ターンを実装して駒を動かしたら切り替わるようにしていきます。

main.dart
late List<List<dynamic>> shogiBoard; // 盤面管理用の配列
ShogiPiece? selectedPiece; // 選択されている駒
int selectedRow = -1; // 選択されている駒の行番号
int selectedCol = -1; // 選択されている駒の列番号
List<List<int>> validMoves = []; // 選択中の駒が移動可能な座標
bool isAllyTurn = true; // 味方のターンかどうか

まずは自分のターンかどうかの変数を用意。

main.dart
// ターン切り替え
void turnChange() {
  setState(() {
    isAllyTurn = !isAllyTurn;
  });
}

ターン切り替え用メソッドも作成。

main.dart
// 駒を移動
void movePiece(int newRow, int newCol) {
  shogiBoard[newRow][newCol] = selectedPiece; // 新しい座標へ移動
  shogiBoard[selectedRow][selectedCol] = null; //元の座標を初期化

  // 現在の選択をリセット
  setState(() {
    selectedPiece = null;
    selectedRow = -1;
    selectedCol = -1;
    validMoves = [];
  });

  turnChange();
}

駒を移動させた後にターン切り替えメソッドを呼ぶように。

main.dart
// ピースを選択する
void selectPiece(int row, int col) {
  // 駒を選択していない状態から駒を選択した時
  if (selectedPiece == null && shogiBoard[row][col] != null) {
    if (shogiBoard[row][col]!.isAlly == isAllyTurn) {
      setState(() {
        selectedPiece = shogiBoard[row][col];
        selectedRow = row;
        selectedCol = col;
      });
    }
    // 駒を選択している状態で自陣の他の駒を選択した時
  } else if (shogiBoard[row][col] != null && shogiBoard[row][col]!.isAlly == selectedPiece!.isAlly) {
    setState(() {
      selectedPiece = shogiBoard[row][col];
      selectedRow = row;
      selectedCol = col;
    });
    // 移動可能な座標を選択した時
  } else if (selectedPiece != null && validMoves.any((coordinate) => coordinate[0] == row && coordinate[1] == col)) {
    movePiece(row, col);
  }

  validMoves = calculateValidMoves(shogiBoard, selectedRow, selectedCol, selectedPiece); // 移動可能な座標を計算
}
if (shogiBoard[row][col]!.isAlly == isAllyTurn)

さらに自分のターンでは自陣の駒、相手のターンでは敵陣の駒しか選択できないように。

if (shogiBoard[row][col] != null && shogiBoard[row][col]!.isAlly == selectedPiece!.isAlly)

すでに駒を選択していても、自分の他の駒であれば選択を切り替えられるようにしました。

main.dart

Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
	// ターン表示
        Text(
          isAllyTurn ? "あなたのターンです" : "相手のターンです",
          style: const TextStyle(
            fontWeight: FontWeight.bold,
          ),
        ),
        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;
            bool isSelected = row == selectedRow && col == selectedCol; // 座標が選択されているかどうか
            bool isValidMove = false;

            // 選択中の駒が移動可能な座標かどうか
            for (var position in validMoves) {
              if (position[0] == row && position[1] == col) {
                isValidMove = true;
              }
            }

            return Square(
              shogiPiece: shogiBoard[row][col],
              isSelected: isSelected,
              onTap: () => selectPiece(row, col),
              isValidMove: isValidMove,
            );
          },
        ),
      ],
    )
  );
}

今どちらのターンなのか分かるように簡単にですが表示しておきましょう。

iOS シミュレータ
自分のターン

iOS シミュレータ
相手のターン

終わりに

ここまでお疲れさまでした〜!
ようやく駒を動かせるようになり、ターンも切り替わるので対戦に近いことができるようになりました。

次回は相手の駒を取れるようにして、より対戦感を強めたいと思います。
https://zenn.dev/flutteruniv_dev/articles/8a312580b3245c

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

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

Twitter

https://twitter.com/minase_hiro_

Threads

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

Flutter大学

Discussion