2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FlutterAdvent Calendar 2022

Day 18

ChatGPTにflutterのコードを書かせてみた

Last updated at Posted at 2022-12-17

ChatGPT

ChatGpt 楽しいですよね。対話が成立しているのか、していないのか、絶妙なところで絶妙な回答をしてくれてクセはありますが可能性を感じます。

早くからコードを出力させたり、コードをデバッグさせてみたり、コードから説明を引き出してみたりしている人がいて、自分でもJavaScriptのコードを書かせてみたりしていました。

Stack OverflowでChatGPTによる回答を一時的に禁止されたというニュースを見ましたが実際、そのまま動くコードが出てくるかと思ったら、なんだこれ? と首をかしげるコードを吐いてくれたりとブレが大きいのは確かです。

じゃあ、flutterのコードはどうだろう? と先のツイートと同じ「15パズル」のコードを試しに吐かせてみました。

先にできあがったコードを置いておきます。

ガワを用意しよう

$ flutter create puzzle_app

AIに聞いてみよう

はじめに

ChatGPTへの質問です。

地の文はChatGPTからの回答です。

やり取りへのコメントです。

ここからChatGPTとのやり取り

flutterで動作する15パズルのコードを提示して。

Flutterを使って15パズルを作成するには、次のような方法があります。

ライブラリをインポートする

Flutterには、15パズルを実装するために必要なライブラリが用意されています。最初に、次のように import を使ってこれらのライブラリをインポートします。

main.dart
import 'package:flutter/material.dart';
import 'dart:math';

このように順序立てて、チュートリアルとしてコードを提示してくれる例はめずらしかったりします。

ステートフルウィジェットを作成する

15パズルは、ゲームの状態を保持する必要があります。そのために、Flutterではステートフルウィジェットを使用します。次のように、 StatefulWidget クラスを継承したクラスを作成します。

main.dart
class PuzzleWidget extends StatefulWidget {
  @override
  _PuzzleWidgetState createState() => _PuzzleWidgetState();
}

ステートクラスを作成する

次に、ステートクラスを作成します。このクラスは、ゲームの状態を保持し、UIを更新するために必要です。

main.dart
class _PuzzleWidgetState extends State<PuzzleWidget> {
  // ゲームの状態を保持する変数をここに定義する

  @override
  Widget build(BuildContext context) {
    // UIをここで構築する
  }
}

ゲームの状態を保持する変数を定義する

15パズルのゲームの状態を保持するために、変数を定義します。例えば、盤面を表すために、2次元配列を使用することができます。また、ゲームが完了したかどうかを表すフラグも必要です。

main.dart
class _PuzzleWidgetState extends State<PuzzleWidget> {
  // 盤面を表す2次元配列
  List<List<int>> board;
  // ゲームが完了したかどうかを表すフラグ
  bool isCompleted;

  @override
  Widget build(BuildContext context) {
    // UIをここで構築する
  }
}

UIを構築する

ステートクラスの build メソッド内で、15パズルのUIを構築します。Flutterでは、 Widget を使用してUIを構築することができます。
例えば、盤面を表示するために、 GridView を使用することができます。GridView には、盤面を表す2次元配列を渡し、それを使用して盤面を構築することができます。

main.dart
class _PuzzleWidgetState extends State<PuzzleWidget> {
  // 省略

  @override
  Widget build(BuildContext context) {
    return GridView.count(
      crossAxisCount: 4,
      children: List.generate(16, (index) {
        final x = index % 4;
        final y = index ~/ 4;
        final value = board[y][x];
        return Container(
          decoration: BoxDecoration(
            border: Border.all(color: Colors.black),
          ),
          child: value == 0
              ? null
              : Center(
                  child: Text(
                    '$value',
                    style: TextStyle(fontSize: 20),
                  ),
                ),
        );
      }),
    );
  }
}

ゲームの操作を実装する

15パズルをプレイするために、ユーザーがゲームを操作できるようにする必要があります。例えば、ユーザーがタップしたときに、タップした場所にあるタイルを移動させることができます。
このような操作を実装するには、 GestureDetector を使用することができます。 GestureDetector は、特定のジェスチャー(タップやフリックなど)が検出されたときに、コールバック関数を呼び出すことができます。

また、ゲームの操作を受け付けるために、必要な変数も定義します。例えば、空のタイルの位置を保持するために、座標を表す x と y の変数が必要です。

main.dart
class _PuzzleWidgetState extends State<PuzzleWidget> {
  // 省略

  // 空のタイルの位置を表す座標
  int emptyTileX;
  int emptyTileY;

  @override
  Widget build(BuildContext context) {
    // UIをここで構築する
  }
}

これらの変数を使用して、ゲームの状態を保持し、操作を受け付けることができます。

途中、停まったので何度か「続き」とタイプしましたが、ここまで一気に教えてくれました。
変数が初期化されていなかったので、その補間を要求します。

board、isCompleted、emptyTileX、emptyTileYを初期化して。

boardisCompletedemptyTileXemptyTileY を初期化するには、次のようにします。

main.dart
  // 盤面を表す2次元配列
  List<List<int>> board = [
      [1, 2, 3, 4],
      [5, 6, 7, 8],
      [9, 10, 11, 12],
      [13, 14, 15, 0],
    ];
  // ゲームが完了したかどうかを表すフラグ
  bool isCompleted = false;
  // 空のタイルの位置を表す座標
  int emptyTileX = 3;
  int emptyTileY = 3;

これらの変数を初期化することで、15パズルのゲームの状態を保持することができます。

この例だと問題ないのですが、初期化をinitState() 内で初期化しようとする例もありましたが、それだと今のDartでは元の変数定義にlate 修飾子が必要になります。

中味が無いonTap()を要求します。

onTapを実装して。

onTap コールバック関数を実装するには、次のようにします。

main.dart
onTap: () {
  // 空のタイルとタップされたタイルが隣り合っている場合にのみ、タイルを移動させる
  if ((emptyTileX == x && (emptyTileY == y - 1 || emptyTileY == y + 1)) ||
      (emptyTileY == y && (emptyTileX == x - 1 || emptyTileX == x + 1))) {
    // タイルを移動する処理をここに記述する
  }
  // 空のタイルの位置を更新する
  emptyTileX = x;
  emptyTileY = y;
  // タップされたタイルの位置を空のタイルの位置に更新する
  board[y][x] = 0;
  // 盤面を更新する
  setState(() {});
},

このようにして、onTap コールバック関数を実装することで、ユーザーがタップしたときに、タイルを移動させることができます。

このonTap()の処理には不具合があります(後述)
引き続いてonTap()関連の処理が出力されてきました。

ただし、ここで紹介したのは、一つのタイルを移動させるだけの処理です。完全な15パズルのゲームを実装するには、さらに次のような処理が必要です。

ゲームが完了したかどうかを判定する処理

ゲームが完了したかどうかを判定する処理は、次のように記述します。
盤面が初期状態と同じかどうかを判定する
isCompleted 変数を更新する

main.dart
// 盤面が初期状態と同じかどうかを判定する
bool isBoardCompleted() {
  for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 4; x++) {
      if (board[y][x] != y * 4 + x + 1) {
        return false;
      }
    }
  }
  return true;
}

// ゲームが完了したかどうかを判定し、isCompleted 変数を更新する
void checkCompleted() {
  isCompleted = isBoardCompleted();
  setState(() {});
}

このisBoardCompletedには不具合があります(後述)

盤面をシャッフルする処理

盤面をシャッフルする処理は、次のように記述します。

main.dart
  // 盤面をシャッフルする
  void shuffle() {
    // 空のタイルを含むリストを作成する
    List<int> tiles = List.generate(16, (i) => i);
    tiles.remove(0);
    tiles.add(0);
    // シャッフルする
    tiles.shuffle();
    // シャッフルされたリストをもとに、盤面を更新する
    for (int i = 0; i < 4; i++) {
      for (int j = 0; j < 4; j++) {
        board[i][j] = tiles[i * 4 + j];
        // 空のタイルの位置を記録する
        if (board[i][j] == 0) {
          emptyTileX = j;
          emptyTileY = i;
        }
      }
    }
    // 盤面を更新する
    setState(() {});
  }

これに引き続き、聞いてもいないのに完全なコードを出力しようとしてくれます。

完全なコード例は次のようになります。

main.dart
import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(PuzzleApp());
}

class PuzzleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: PuzzleWidget(),
    );
  }
}

class PuzzleWidget extends StatefulWidget {
  @override
  _PuzzleWidgetState createState() => _PuzzleWidgetState();
}

class _PuzzleWidgetState extends State<PuzzleWidget> {
  // 盤面を表す2次

残念ながら通信エラーである程度以上長いコードブロックは、うまく出力してくれません。
「続き」と要求してもコードブロックをはじめから出力しようとするんですよね。
なので、initState とか、blild とか関数毎に出力させました。
buildの中にさっきなかったScaffoldとか、解説だけに言及があったGestureDetectorとかが含まれていて、途中提示されたコードは要約だったのかな? とも。

build()にシャッフルボタンが追加されていたのですが、ここに古いRaisedButtonが利用されていました。

build()
// シャッフルボタンを表示する
Container(
  margin: EdgeInsets.all(10),
  child: RaisedButton(
    child: Text(
      'Shuffle',
      style: TextStyle(fontSize: 20),
    ),
    onPressed: () {
      // シャッフル処理
    },
  ),
),

なんどか置き換えを要望してみたのですが応えてくれず、手動でElevatedButtonに置き換えます。

動かしてみます。
とりあえず動きました。
ただ、パネルを交換すると双方が真っ白になります。

スクリーンショット 2022-12-16 16.43.35.png

前述したonTap()の処理に問題があるようです。直してもらいましょう。

onTapの中でboardの元の値が消えるのは間違っていませんか?

おそらく、15パズルでのタイルの移動について質問されていると思われますが、そのような記述はありませんでしたので、正しいコードを提示させていただきます。

15パズルでは、タイルをタップしても、元の値が消えることはありません。タイルをタップすると、空のタイルとタップされたタイルが隣り合っている場合にのみ、タイルを移動させることができます。その際には、空のタイルの位置を更新し、盤面を更新することで、タイルの移動を実現します。

以下に、例を示します。

onTap()
  // 空のタイルとタップされたタイルが隣り合っている場合にのみ、タイルを移動させる
  if ((emptyTileX == x && (emptyTileY == y - 1 || emptyTileY == y + 1)) ||
      (emptyTileY == y && (emptyTileX == x - 1 || emptyTileX == x + 1))) {
    // 空のタイルの位置を更新する
    emptyTileX = x;
    emptyTileY = y;
    // 空のタイルを移動させる
    int temp = board[y][x];
    board[y][x] = board[emptyTileY][emptyTileX];
    board[emptyTileY][emptyTileX] = temp;
    // 盤面を更新する
    setState(() {});
    // ゲームが完了したかどうかを判定する
    checkCompleted();
  }

とぼけながらも、修正してきました。
さらに、ゲームの完了チェックisBoardCompletedがうまくいってません。

isBoardCompletedは空のタイルを考慮していない。

15パズルで、ゲームが完了したかどうかを判定する際に、空のタイルを考慮するには、次のようにします。

// ゲームが完了したかどうかを判定する
bool isBoardCompleted() {
  for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 4; j++) {
      // 空のタイルは特別扱いする
      if (i == 3 && j == 3) {
        continue;
      }
      if (board[i][j] != i * 4 + j + 1) {
        return false;
      }
    }
  }
  return true;
}

上記のコードでは、空のタイルの位置を特別扱いしています。具体的には、i == 3 && j == 3 のように、空のタイルの位置を特定し、その位置である場合には、次のタイルを判定するために continue を呼び出します。これにより、空のタイルを考慮して、ゲームが完了したかどうかを判定することができます。

修正してくれました。
さっきとforループで利用する変数が違ってますが気にしません。

問題なく動くようになったのですが、ゲームの完了はcheckCompletedで判定してるだけで寂しかったのでリアクションを追加してもらいます。

コンプリートした時にお祝い表示を追加して。

15パズルを完成したときに、お祝いの表示を追加するには、build 関数内で、ゲームが完了したかどうかを判定し、完了している場合には、お祝いの表示を追加するようにします。

例えば、次のように記述することで、ゲームが完了したときに「Congratulations!」と表示されるようにできます。

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('15 Puzzle'),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          // 盤面を表示する
          Expanded(
            child: GridView.count(
              crossAxisCount: 4,
              children: List.generate(16, (index) {
                // 盤面を表示するコード
              }),
            ),
          ),
          // お祝いの表示を追加する
          if (isCompleted)
            Text(
              'Congratulations!',
              style: TextStyle(fontSize: 20),
            ),
          // シャッフルボタンを表示する
          Container(
            margin: EdgeInsets.all(10),
            child: RaisedButton(
              child: Text(
                'Shuffle',
                style: TextStyle(fontSize: 20),
              ),
              onPressed: () {
                // シャッフル処理
              },
            ),
          ),
        ],
      ),
    ),
  );
}

このようにすることで、ゲームが完了したときに、お祝いの表示を追加することができます。

ちょっと寂しいですけど追加されました。

完成しました

スクリーンショット 2022-12-16 15.31.13.png

ちゃんと(地味なりに)コングラチュレーション表示もしてくれます。

スクリーンショット 2022-12-16 15.33.16.png

iPhone(エミュ)でも動きました。

スクリーンショット 2022-12-16 15.45.11.png

まとめ

意外とちゃんとコードを書いてくれます。
今回の例は、12月15日にシャッフルや、完了処理までエラー無くたどり着けたものを掲載しました。

ただ学習元がちょっと古いのか、null saftyは考慮してくれないことが多い印象です。
そして同じ質問をしても、まったく同じコード、同じチュートリアルを出してくれるわけではありません。いくつかバリエーションがあって、もったいぶって省略してくれたり、たまに「flutterなんか知らない」と言われることもありました。
あとちょうどこの記事を書こうと試行を始めた頃に「network error」でAIとのコミュニケーションが途絶える問題があって難儀しました。これ12月15日頃に改善されています。

ChatGPT

他にも、前からどう実装仕様と思っていた機能を質問すると少し調べてたどり着けなかった情報を提示してくれたり検索がわりに使えそうな気がしています。

おまけ

そうそう、そういえば1度だけpubspec.yamlから出力を始めたことがありました。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_puzzle: ^0.0.2

この直後に「network error」でその全貌を確認することはできませんでしたが、そんなパッケージあるの? と確認するとありました。

3年前の古いパッケージでwebにも非対応だったので未確認なのですが、ざっくりコードを眺めると画像を読み込んでパネルパズルにしてくれるようです。よくこんなのまで網羅しているなと感心。1度だけでしたけど。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?