♟️

Flutter で将棋ゲームを作ろう #5 持ち駒を盤面に出そう

2023/07/27に公開

はじめに

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

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

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

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

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

持ち駒を選択できるようにしよう

前回の記事では相手のコマを取れるようになり、持ち駒も表示できるようになりましたね。
https://zenn.dev/flutteruniv_dev/articles/8a312580b3245c

今回はその駒を盤面に出す処理を作っていきます。

最初にどの持ち駒を盤面に出すか、選べるようにしましょう!

main.dart
GridView.builder(
  shrinkWrap: true,
  itemCount: piecesTakenByAlly.length,
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 9),
  physics: const NeverScrollableScrollPhysics(),
  itemBuilder: (context, index) => GestureDetector(
    onTap: () {
      selectDropPosition(piecesTakenByAlly[index]);
    },
    child: Padding(
      padding: const EdgeInsets.all(5.0),
      child: Image.asset(piecesTakenByAlly[index].imagePath),
    ),
  ),
),

まずは GestureDetector の onTap で selectDropPosition というメソッドを呼ぶように。

main.dart
bool isSelectingDropPosition = false; // 手持ちの駒を打とうとしている

// ... 省略

// 手持ちの駒を取りどこに打つか決める
void selectDropPosition(ShogiPiece piece) {
  if (isAllyTurn == piece.isAlly) {
    setState(() {
      isSelectingDropPosition = true;
      selectedPiece = piece;
    });
  }
}

合わせて isSelectingDropPosition という bool の変数を用意し、 selectDropPosition メソッドを作成しました。

main.dart
GridView.builder(
  shrinkWrap: true,
  itemCount: piecesTakenByAlly.length,
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 9),
  physics: const NeverScrollableScrollPhysics(),
  itemBuilder: (context, index) => GestureDetector(
    onTap: () {
      selectDropPosition(piecesTakenByAlly[index]);
    },
    child: Container(
      decoration: BoxDecoration(
        border: Border.all(
          width: 2.0,
          color: isSelectingDropPosition && selectedPiece == piecesTakenByAlly[index] ? Colors.black26 : Colors.transparent,
        ),
      ),
      child: Padding(
        padding: const EdgeInsets.all(5.0),
        child: Image.asset(piecesTakenByAlly[index].imagePath),
      ),
    ),
  ),
),

次に選択している駒がどれなのか分かるようにします。

border: Border.all(
  width: 2.0,
  color: isSelectingDropPosition && selectedPiece == piecesTakenByAlly[index] ? Colors.black26 : Colors.transparent,
),

選択中の駒だった場合は Border が表示されるようにしました。

iOS シミュレータ
こうなりますね。

打てる範囲を可視化しよう

選択している駒を打てる場所の色を変えるようにします。

main.dart
return Square(
  piece: board[row][col],
  isSelected: isSelected,
  isValidMove: isValidMove,
  onTap: () => selectPiece(row, col),
  isSelectingDropPosition: isSelectingDropPosition,
);

isSelectingDropPosition を Square に渡します。

square.dart
class Square extends StatelessWidget {
  final ShogiPiece? shogiPiece;
  final bool isSelected; // 選択されているかどうか
  final void Function()? onTap; // タップされた時に呼ばれるメソッド
  final bool isValidMove; // 選択中の駒が移動可能か
  final bool isSelectingDropPosition; // 手持ちの駒を打とうとしているか

  const Square({
    super.key,
    required this.shogiPiece,
    required this.isSelected,
    required this.onTap,
    required this.isValidMove,
    required this.isSelectingDropPosition,
  });

  
  Widget build(BuildContext context) {
    Color? squareColor;

    // 座標の状態によって背景色を変化
    if (isSelectingDropPosition && shogiPiece == null) {
      squareColor = Colors.green[100]; // 選択中の駒を打つことができる
    } else if (isSelected) {
      squareColor = Colors.green; // 駒を選択中
    } else if (isValidMove) {
      squareColor = Colors.green[100]; // 選択中の駒が移動可能
    } else {
      squareColor = Colors.orange[100]; // 盤面の色
    }

square.dart では、 isSelectingDropPosition を受け取り
isSelectingDropPosition かつ駒が存在しない座標であれば背景色を緑色に変化させるようにしました。

iOS シミュレータ
このようになりますね。
(厳密にいうと二歩や動けなくなる位置に置けない、など考慮する点がいくつかあるんですが、ここでは簡略化して駒が存在しない座標全てに置けるようにしています)

盤面に反映させよう

さていよいよ盤面に打っていきます。
盤面を選択した時のメソッド selectPiece がすでにあるので、条件を追加します。

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);
    // 持ち駒を空き座標に打った時
  } else if (isSelectingDropPosition && shogiBoard[row][col] == null) {
    // 空き座標にセット
    shogiBoard[row][col] = selectedPiece;

    // 持ち駒から削除
    if (selectedPiece!.isAlly) {
      piecesTakenByAlly.remove(selectedPiece);
    } else {
      piecesTakenByEnemy.remove(selectedPiece);
    }
    
    // 現在の選択をリセット
    selectedPiece = null;
    selectedRow = -1;
    selectedCol = -1;
    validMoves = [];
    isSelectingDropPosition = false;
    
    turnChange();
  }
// 持ち駒を空き座標に打った時
} else if (isSelectingDropPosition && shogiBoard[row][col] == null) {
  // 空き座標にセット
  shogiBoard[row][col] = selectedPiece;

  // 持ち駒から削除
  if (selectedPiece!.isAlly) {
    piecesTakenByAlly.remove(selectedPiece);
  } else {
    piecesTakenByEnemy.remove(selectedPiece);
  }
  
  // 現在の選択をリセット
  selectedPiece = null;
  selectedRow = -1;
  selectedCol = -1;
  validMoves = [];
  isSelectingDropPosition = false;
  
  turnChange();
}

isSelectingDropPosition かつ 選択した座標が空だった場合、
座標へのセットと持ち駒からの削除をおこないます。

同時に、現在選択している駒や座標をクリアしつつ
isSelectingDropPosition も false にして持ち駒を打つモードを終了。

最後に相手へターンを渡します。

iOS シミュレータ
これで無事に相手から奪った角を自分の駒として打つことができました。

終わりに

ここまでお疲れさまでした〜!
相手の駒を取って、持ち駒を打てるようになったことで戦略性がぐっと増しましたね。
次回は「成り」を実装してさらに複雑にしていきます。

質問などあれば気軽にコメントください!

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

  • 王手の実装
  • 詰みの判定
  • CPUを実装する
  • 「二歩」の禁止
  • 棋譜を記録する

Twitter

https://twitter.com/minase_hiro_

Threads

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

Flutter大学

Discussion