ChatGPT
ChatGpt 楽しいですよね。対話が成立しているのか、していないのか、絶妙なところで絶妙な回答をしてくれてクセはありますが可能性を感じます。
早くからコードを出力させたり、コードをデバッグさせてみたり、コードから説明を引き出してみたりしている人がいて、自分でもJavaScriptのコードを書かせてみたりしていました。
Stack OverflowでChatGPTによる回答を一時的に禁止されたというニュースを見ましたが実際、そのまま動くコードが出てくるかと思ったら、なんだこれ? と首をかしげるコードを吐いてくれたりとブレが大きいのは確かです。
じゃあ、flutterのコードはどうだろう? と先のツイートと同じ「15パズル」のコードを試しに吐かせてみました。
先にできあがったコードを置いておきます。
ガワを用意しよう
$ flutter create puzzle_app
AIに聞いてみよう
はじめに
ChatGPTへの質問です。
地の文はChatGPTからの回答です。
やり取りへのコメントです。
ここからChatGPTとのやり取り
flutterで動作する15パズルのコードを提示して。
Flutterを使って15パズルを作成するには、次のような方法があります。
ライブラリをインポートする
Flutterには、15パズルを実装するために必要なライブラリが用意されています。最初に、次のように import を使ってこれらのライブラリをインポートします。
import 'package:flutter/material.dart';
import 'dart:math';
このように順序立てて、チュートリアルとしてコードを提示してくれる例はめずらしかったりします。
ステートフルウィジェットを作成する
15パズルは、ゲームの状態を保持する必要があります。そのために、Flutterではステートフルウィジェットを使用します。次のように、 StatefulWidget クラスを継承したクラスを作成します。
class PuzzleWidget extends StatefulWidget {
@override
_PuzzleWidgetState createState() => _PuzzleWidgetState();
}
ステートクラスを作成する
次に、ステートクラスを作成します。このクラスは、ゲームの状態を保持し、UIを更新するために必要です。
class _PuzzleWidgetState extends State<PuzzleWidget> {
// ゲームの状態を保持する変数をここに定義する
@override
Widget build(BuildContext context) {
// UIをここで構築する
}
}
ゲームの状態を保持する変数を定義する
15パズルのゲームの状態を保持するために、変数を定義します。例えば、盤面を表すために、2次元配列を使用することができます。また、ゲームが完了したかどうかを表すフラグも必要です。
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次元配列を渡し、それを使用して盤面を構築することができます。
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 の変数が必要です。
class _PuzzleWidgetState extends State<PuzzleWidget> {
// 省略
// 空のタイルの位置を表す座標
int emptyTileX;
int emptyTileY;
@override
Widget build(BuildContext context) {
// UIをここで構築する
}
}
これらの変数を使用して、ゲームの状態を保持し、操作を受け付けることができます。
途中、停まったので何度か「続き」とタイプしましたが、ここまで一気に教えてくれました。
変数が初期化されていなかったので、その補間を要求します。
board、isCompleted、emptyTileX、emptyTileYを初期化して。
board
、isCompleted
、emptyTileX
、emptyTileY
を初期化するには、次のようにします。
// 盤面を表す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 コールバック関数を実装するには、次のようにします。
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 変数を更新する
// 盤面が初期状態と同じかどうかを判定する
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
には不具合があります(後述)
盤面をシャッフルする処理
盤面をシャッフルする処理は、次のように記述します。
// 盤面をシャッフルする
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(() {});
}
これに引き続き、聞いてもいないのに完全なコードを出力しようとしてくれます。
完全なコード例は次のようになります。
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
が利用されていました。
// シャッフルボタンを表示する
Container(
margin: EdgeInsets.all(10),
child: RaisedButton(
child: Text(
'Shuffle',
style: TextStyle(fontSize: 20),
),
onPressed: () {
// シャッフル処理
},
),
),
なんどか置き換えを要望してみたのですが応えてくれず、手動でElevatedButton
に置き換えます。
動かしてみます。
とりあえず動きました。
ただ、パネルを交換すると双方が真っ白になります。
前述したonTap()
の処理に問題があるようです。直してもらいましょう。
onTapの中でboardの元の値が消えるのは間違っていませんか?
おそらく、15パズルでのタイルの移動について質問されていると思われますが、そのような記述はありませんでしたので、正しいコードを提示させていただきます。
15パズルでは、タイルをタップしても、元の値が消えることはありません。タイルをタップすると、空のタイルとタップされたタイルが隣り合っている場合にのみ、タイルを移動させることができます。その際には、空のタイルの位置を更新し、盤面を更新することで、タイルの移動を実現します。
以下に、例を示します。
// 空のタイルとタップされたタイルが隣り合っている場合にのみ、タイルを移動させる
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: () {
// シャッフル処理
},
),
),
],
),
),
);
}
このようにすることで、ゲームが完了したときに、お祝いの表示を追加することができます。
ちょっと寂しいですけど追加されました。
完成しました
ちゃんと(地味なりに)コングラチュレーション表示もしてくれます。
iPhone(エミュ)でも動きました。
まとめ
意外とちゃんとコードを書いてくれます。
今回の例は、12月15日にシャッフルや、完了処理までエラー無くたどり着けたものを掲載しました。
ただ学習元がちょっと古いのか、null saftyは考慮してくれないことが多い印象です。
そして同じ質問をしても、まったく同じコード、同じチュートリアルを出してくれるわけではありません。いくつかバリエーションがあって、もったいぶって省略してくれたり、たまに「flutterなんか知らない」と言われることもありました。
あとちょうどこの記事を書こうと試行を始めた頃に「network error」でAIとのコミュニケーションが途絶える問題があって難儀しました。これ12月15日頃に改善されています。
ChatGPT
他にも、前からどう実装仕様と思っていた機能を質問すると少し調べてたどり着けなかった情報を提示してくれたり検索がわりに使えそうな気がしています。
おまけ
そうそう、そういえば1度だけpubspec.yaml
から出力を始めたことがありました。
dependencies:
flutter:
sdk: flutter
flutter_puzzle: ^0.0.2
この直後に「network error」でその全貌を確認することはできませんでしたが、そんなパッケージあるの? と確認するとありました。
3年前の古いパッケージでwebにも非対応だったので未確認なのですが、ざっくりコードを眺めると画像を読み込んでパネルパズルにしてくれるようです。よくこんなのまで網羅しているなと感心。1度だけでしたけど。