
ChatGPTに簡単なトランプゲームを作らせてみた
本投稿は2023.03.03 投稿のクリエーションライン 技術ブログからの転載となります。
ChatGPTはプログラミングもできるということなので、実際にプログラムを作成させてみましょう。
プログラマーが不要になる世界は、近いうちに来るのでしょうか?
TL;DR
指示に従って、それなりのプログラムを書いてくれる。
たまにバグがある。(初級プログラマーと同等程度という印象。)
指示していない機能を勝手に追加してくることもある。
変更指示も、それなりに対応してくれる。
変更を依頼していない箇所まで勝手に変更してくることがある。
以前変更した箇所が元のコードに戻されてしまうことがある。
ゴミコードが残ってしまうことがある。(初級プログラマーと同等程度という印象。)
それなりに高度なこともできる。(中級~上級プログラマーと同等程度という印象。)
ValueObjectへの変更を指示するだけで、ある程度適切に実現してくれた。
TypeScriptには標準でシャッフル機能がないが、Fisher-Yatesアルゴリズムで実装してくれた。
指示を出す側に適切なスキルがあれば、半分自動で作ってくれるというイメージ。
やってみよう
まずは小手調べで、トランプのベースから作ってみます。

問題なさそうですね。
特に、下記の点が素晴らしいです。
トランプについて細かい説明をしていないにもかかわらず、マークと番号を組み合わせて52種類のカードを生成してくれている。
J・Q・K・Aという特殊カードもしっかりと対応されている。
通常の二重ループの代わりに、リスト内包表記を使って簡潔に実装されている。
変数名の付け方も問題なし。
続けていきます。

関数化も問題ありません。
使い方の例も教えてくれました。
次は、山札から引いていくことを想定して実装を進めてみます。


なるほど、引かれたカードを覚えておいて、次回はそれ以外から選んで返すようにするのですね。
・・・と思ったら、引いた側がカードを覚えておいて、次回に引く際に「これ以外から引いてね」とお願いする必要があるようです。
機能的には仕様を満たしていますが、設計としてはかなり汚いと言わざるを得ません。
さて、今回は山札から引いていくとカードの残りが足りなくなる可能性があるので、それに対する処理を追加してくれました。
このあたりは初心者プログラマーが見落としがちな観点ですが、きちんと対応してくれていて素晴らしいですね。
・・・が、そもそも最初に関数化した時点で、52枚を超える枚数を指定された場合にも例外が発生してしまうので、この処理はその時点で追加されているとベターでした。
次に、山札をリセットできるようにしておきましょう。
そもそも引いた側が覚えておくという設計になっているので、山札をリセットするという概念とは相容れないのですが、そこを含めて一気に修正してくれることを期待します。


目論見成功です。
山札が自分で引かれたカードを管理してくれるようになり、リセットする機能も問題なく実装されました。
実装方法の具体的な指示は出していませんが、Deckクラスを導入し、今まで関数だった部分をそのクラスのメソッドにしてくれました。
ところで、ChatGPTは回答が長くなると尻切れになってしまうので、そんなときはいつもの「続きは?」で聞き出します。

「# カード」と書かれていた部分が「# 使用済みカードをリセットする」に変わったような気がしますが、特に問題ないでしょう。
いつものように解説や使い方の例も教えてくれています。
今度は少し仕様を変更してみます。



まず、リセットする機能自体はうまく実装されているようです。
説明や使い方の例も問題ないでしょう。
しかし、52枚より多い枚数を指定するとValueErrorが発生してしまいます。
最初に関数化したときと同じ問題です。
いったんその部分は無視して、次に進めることにします。
ここまでに作ったトランプを利用して、簡単なゲームを作り込んでいくことにします。


今までの部分は変更がないにもかかわらず再掲されていて、無駄に長い回答になっています。
ゲームのコア部分は、Playerクラスと、それを使ったゲームの流れが実装されました。
仕様を満たす実装としては、だいたいOKでしょう。ただし・・・
A・J・Q・Kの得点計算に誤りがあります。おそらく、ネットで紹介されている他の得点計算方法に汚染されてしまっているのでしょう。
ゲームロジックの一部がPlayerの中に実装されてしまっています。動作は問題ありませんが、設計上は減点対象です。
具体的には、カードを引く枚数や勝ち判定の閾値がPlayerの中に記述されていますが、ゲームロジック側に記述するべきです。

説明は分かりやすいです。
ただし、最後の一分は不要でしょう。ChatGPTはいつもこのようなまとめをしたがりますね。
さて、得点計算を修正していきます。

修正内容はよさそうですね。
・・・と思っていたら、ここで今までのコードがまた全掲載されました。


・・・もういいです。そこは分かっています。しつこいです。
続いてJ・Q・Kの得点計算も修正させますが、変更点のみを教えてもらうように伝えておきます。

修正内容はナイスです。
しかし・・・


・・・どうしても全部言わないと気が済まないようです。
さて、今度はコードの内容について少しだけ質問してみましょう。

その通りです。
このプログラムでは、rankはintではなくstrなのです。
ChatGPTはしっかりとそこを踏まえてカバーしてくれていたのでした。
では、分かりやすいように型ヒントを付けてもらいましょう。



尻切れになっていますが、今回は気にしないことにします。
ヒントの付け具合はナイスなのですが、書き方が古いですね。
最近のPythonであればlist[str]と書くべきところを、from typing import Listを使ってList[str]と書いています。
ChatGPTは新しいことをあまり知らないので、このあたりは仕方ないところです。
プログラミング言語やライブラリなどの新しいバージョンについて知りたい場合は、注意が必要です。
さて今度は、やや高度な設計に変更していきます。
ここまでカードの内容は文字列で扱われていましたが、これをオブジェクト化していきます。
しかも今回はDDDで有名なValueObjectを使うという指定付きで作らせてみます。

大変素晴らしいです。
カードをValueObjectとして実装してくれました。


残りの部分も、それに合わせて改修してくれました。
また尻切れになったので、続きを聞いていきます。

続きを述べずに、なぜか説明を始めました。
また、Cardクラスが再掲されていますが、なぜか先ほどの実装よりもメソッドが追加されて強化されています。
その後ろは・・・

先ほどと全く同じDeckクラスが再掲されていますね。
そこはよいので、Playerクラスの実装を教えてほしいのですが。

結局、解説や他の実装が追加された分、先ほどより手前で切れてしまいました。
明示的に、Playerクラスだけを教えてもらうことにします。

今度はうまくいきそうです。先ほどの続きも見え始めました。

OKです。Playerクラスの内容が判明しました。
なぜかdiscardという新しいメソッドが追加されていますが、いったん無視します。
さて、ValueObjectにしてもらったので、さらに洗練させるため、カードの数字をintに変更してみましょう。

よくできました。
ただ、Aが1ではなく14にマッピングされているのは気持ち悪いですね。
これは、後で直してもらうことにしましょう。
そして、続きは・・・

以前のコードは元データがstrだったのをintにキャストしていましたが、今回の改修によって元データがintになったためキャストは不要になったはずです。
説明部分では全く逆のことを言っていますが、コード上は正しくキャストが廃止されています。
rankがintになったのを踏まえて得点計算部分も正しく修正されています。
ただ、もはや判定自体が不要になったということには気づいていないようです。

今度は、勝手に追加された不要なメソッドを削除させます。
ちなみにこのメソッドは、Deckクラスには存在しないはずのreturn_cardメソッドを呼び出しています。


削除はうまくいったようですが、説明はイマイチです。
続いて、Aを14ではなく1と対応させるように変更させます。

問題なさそうです。

得点計算部分も14から1に変更されているので、動作上は問題ありませんが、もはやこの判定は不要なはずですね。
おや、J・Q・Kに関する得点の扱いが先祖返りしてバグりましたね。

改修した内容をしれっと説明していますが、元のコードが正しくて、今回の改修は誤りです。
元に戻すように伝えてみます。

分かったような返答をしていますが、コードは直っていません。

修正しましたと言っていますが、修正前のコードと何も変わっていません。
もう少し具体的に指示してみましょう。

成功です。直りました。

説明も正しいです。
今度は、もはや不要となった条件分岐を廃止してコードをきれいにしていきます。

OKです。今回は一発でうまくいきました。

さて、各クラスの実装が(だいたい)うまくいったので、ゲーム本体のロジックに話を戻しましょう。
CardクラスやPlayerクラスなどが再掲されないように、工夫してみます。


成功です。再掲が不要な部分は別ファイルに逃がしてもらいました。
ゲームのロジックも問題ありませんが、なぜかラウンド数をカウントする機能が勝手に追加されています。
除去させましょう。


カウンター変数・表示処理・カウント処理とも、正常に除去成功です。
相変わらず最後の解説は饒舌で冗長です。
さて、ここでゲームに新しい機能を追加してみましょう。


要件はだいたい満たしているように見えますが、いくつか気になる点があります。
ジョーカーをマークがJOKERで数値が空文字列なカードとして扱っているが、設計に無理矢理感が否めない。
カード一式の生成処理が、なぜかリスト内包表記から通常の二重ループに変更された。
ランダムに引くロジックが、以前は引く際にランダムに選んでいたのに、今回は山札を積む際にシャッフルするようになった。
別にどちらでもよいが、指示していない点を勝手に変更されるのは嬉しくない。
Cardの数値は以前strからintに変更してもらったはずだが、またstrに先祖返りした。
得点判定でsuitがJOKERかどうか判定すべきところ、rankがJOKERかどうかで判定している。
実際にジョーカーを引いた場合は、この判定部分がTrueにならず、次のelseブロック内のキャストで空文字列をキャストしようとして例外が発生してしまう。

尻切れですが、今回は無視して次を見ていきます。
今度は、試しにセルフレビューさせてみましょう。

(バグがあったり先祖返りで数字がstrで保持されてしまったりしていますが、)自己評価では結構よいようです。
改善の余地があるか確認してみましょう。

2つ指摘されていますが、どちらもイマイチ意味が分かりませんね。
「デッキをリセットする際に shuffle メソッドを呼び出すようにすることで~問題を回避する」
既にデッキをリセットする際にshuffleメソッドを呼び出すようになっています。
random.shuffleは呼び出す度にランダムな結果を返すため、毎回同じ並び順にシャッフルされるという問題は起こりません。
「 has_won メソッドで、20 を超えた場合にも最後に引いたカードを手札に残す必要がありますが~除去してしまっています。」
has_wonの中ではカードの除去などしていません。
ゲーム終了時に全カードを捨てる処理はありますが、最後のカードだけを除去するような処理はどこにもありません。
勝敗が決まった後に最後のカードを残さなければいけない必要性も不明です。(除去しない方が設計としてはきれいですが。)
では、最後のチャレンジです。
ここまでPythonで書いてきましたが、TypeScriptに書き直させてみましょう。





だいたいOKのようです。特徴的な点を見ていきましょう。
「♡」が「ハート」になっていたりカードの表示がf"{self.rank}{self.suit}"から`${this.suit}の${this.rank}`に変わったりしています。
shuffleは、TypeScriptの標準機能だけでは実現できないためFisher-Yatesアルゴリズムを用いてシャッフルするように実装されています。
カードが足りない際のリセット後にシャッフルする処理が、reset内に移動されています。
山札内で使用済みのカードを管理する方式から、実際に山札から除去する方式に変更されています。
これにより、リセット時には使用済みカードを空に初期化するのではなく、新たなカード一式を再生成する必要が生じています。
その上、カード一式の生成処理はコンストラクター内の処理と完全に重複しています。
まとめ
ここまで、ChatGPTにプログラミングの手伝いをさせてきました。
色々とやってみた結果、ChatGPTは以下のように活用できそうです。
やり方が分からないことを教えてもらう。
仕様を満たす実装(詳細設計)の案としてコードを書かせる。
誰が書いてもほぼ同じになるような「書けばよいだけ」のような部分を代わりに書いてもらう。
関数化など、簡単なリファクタリングを実施させる。
一方、以下のようなことは難しそうです。
人間プログラマーの完全な代替としてプログラムを作らせる。
絶対にバグのないプログラムを作らせる。
完全に指示通りのプログラムを作らせる。
極端に簡潔な指示だけでプログラムを作らせる。
ChatGPTの特徴を理解した上で、効率よくプログラミングの補助をしてもらうのがよさそうですね。
本投稿は2023.03.03 投稿のクリエーションライン 技術ブログからの転載となります。