ファンリピート社のブログFR note

ChatDevを利用してテトリスを生成させた結果とは!?

こんにちは!ファンリピートの加藤です。

今回はX(旧Twitter)などで話題になっていたChatDevを利用してどこまで実用性があるのか自分なりに動かした結果について共有したいと思います。

非常におもしろいので、自分でも使ってみたい方はぜひ参考にしてみてください!

目次
「システムを短納期かつ低予算で作成したい」
ローコード開発に興味がある

ChatDevとは

概要

ChatDev はさまざまな役割を持った従業員がソフトウェア開発をしてくれるというまさにシステム開発企業さながらの実装が個人のPC上で実現できるものです。ここ一年でよく話題に上がってくるOpenAIのAPIと連携することで大規模言語モデル(LLM)に基づいた生成を行ってくれます。

詳しくはこちらを参照してください。

何がすごいのか?

やはりなんといってもコマンドを打って待つだけでアプリ開発を自動で行ってくれるところです。しかし、どの精度まで可能なのかや本当に生成されたアプリが動くのかといった懸念があると思います。それについてこれから私が行った結果から解説していきます!

私が用意したものと環境

準備する値:Open AIのAPIキー(GPT-3.5)

OS:Windows11(操作はPowerShellで行った)

環境:Anaconda3(Python)

参考:https://github.com/OpenBMB/ChatDev/blob/main/README-Japanese.md

ChatDevの使い方

動作手順に関してはChatDevのGithubに手順が書いてあるのでそれに沿ってコマンドを打つだけです!ここでも簡単に説明しておきます。

実際に動かすには?(以下、Windows環境のコマンド)

1.Githubのリポジトリをクローンします。提供されている一連のスクリプトを自分のPCに移すことを言います。

git clone https://github.com/OpenBMB/ChatDev.git

2.ChatDevを動かすにはバージョン3.9以上のPython環境が必要になります。そのために現環境を変化させずに仮想環境に今回必要な環境を用意します。

conda create -n ChatDev_conda_env python=3.9 -y
conda activate ChatDev_conda_env

3.ChatDevというディレクトリに移動して依存関係をインストールさせます。

cd ChatDev
pip3 install -r requirements.txt

4.ご自分で用意したOpenAiのAPIキーを環境変数として加えます。

$env:OPENAI_API_KEY="your_OpenAI_API_key"

*your_OpenAI_API_keyにAPIキーをコピペすればOK!

5.実際にシステム開発を開始させます。これは私が今回入力した例なのですが、最初のダブルクォーテーション([description_of_your_idea])にはどんなアプリが作りたいのかプロンプトを記述し、二つ目のダブルクォーテーション([project_name])にはアプリ名を記述します。

python run.py --task "[description_of_your_idea]" --name "[project_name]"

私は今回テトリスを作ってみたので、その結果を見てみましょう!

生成されたものについて考察

今回、2度の生成を試したので1回目と2回目の違いについても考察していきます。

1回目の生成

1回目では5にあたるコマンドを以下のように入力して生成をさせました。

python run.py –task “画面上部からさまざまな形のブロックが画面下へと落下し、そのブロックを積み重ねていく中で横にそろった列を削除してポイントを加算していくテトリス” –name “Tetris”

ここから「main.py」と「tetris.py」の二つのスクリプトが生成され、それぞれ以下の通りでした。

'''
This is the main file of the Tetris game. It initializes the game and handles the game loop.
'''
import pygame
from tetris import Tetris
def main():
    # Initialize pygame
    pygame.init()
    # Set up the game window
    window_width = 800
    window_height = 600
    window = pygame.display.set_mode((window_width, window_height))
    pygame.display.set_caption("Tetris")
    # Create a Tetris object
    tetris = Tetris(window)
    # Game loop
    running = True
    clock = pygame.time.Clock()
    while running:
        # Handle events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_LEFT:
                    tetris.move_left()
                elif event.key == pygame.K_RIGHT:
                    tetris.move_right()
                elif event.key == pygame.K_DOWN:
                    tetris.move_down()
                elif event.key == pygame.K_UP:
                    tetris.rotate()
        # Update the game state
        tetris.update()
        # Draw the game
        window.fill((0, 0, 0))
        tetris.draw()
        pygame.display.flip()
        # Limit the frame rate
        clock.tick(30)
    # Quit the game
    pygame.quit()
if __name__ == "__main__":
    main()
'''
This file contains the Tetris class which represents the game logic.
'''
import pygame
import random
from piece import Piece
class Tetris:
    def __init__(self, window):
        self.window = window
        self.board_width = 10
        self.board_height = 20
        self.board = [[0] * self.board_width for _ in range(self.board_height)]
        self.current_piece = None
        self.next_piece = None
        self.score = 0
        self.font = pygame.font.Font(None, 36)
        self.spawn_piece()
    def move_left(self):
        if self.current_piece:
            self.current_piece.move_left()
            if self.collides():
                self.current_piece.move_right()
    def move_right(self):
        if self.current_piece:
            self.current_piece.move_right()
            if self.collides():
                self.current_piece.move_left()
    def move_down(self):
        if self.current_piece:
            self.current_piece.move_down()
            if self.collides():
                self.current_piece.move_up()
                self.place_piece()
                self.remove_completed_lines()
                self.spawn_piece()
    def rotate(self):
        if self.current_piece:
            self.current_piece.rotate()
            if self.collides():
                self.current_piece.rotate_back()
    def update(self):
        if self.current_piece:
            self.current_piece.update()
    def draw(self):
        self.draw_board()
        self.draw_current_piece()
        self.draw_next_piece()
        self.draw_score()
    def draw_board(self):
        for row in range(self.board_height):
            for col in range(self.board_width):
                if self.board[row][col] != 0:
                    pygame.draw.rect(self.window, (255, 255, 255), (col * 30, row * 30, 30, 30))
    def draw_current_piece(self):
        if self.current_piece:
            for row in range(4):
                for col in range(4):
                    if self.current_piece.shape[row][col] != 0:
                        pygame.draw.rect(self.window, (255, 255, 255), ((self.current_piece.x + col) * 30, (self.current_piece.y + row) * 30, 30, 30))
    def draw_next_piece(self):
        if self.next_piece:
            for row in range(4):
                for col in range(4):
                    if self.next_piece.shape[row][col] != 0:
                        pygame.draw.rect(self.window, (255, 255, 255), ((self.board_width + col + 2) * 30, (row + 2) * 30, 30, 30))
    def draw_score(self):
        score_text = self.font.render(f"Score: {self.score}", True, (255, 255, 255))
        self.window.blit(score_text, (self.board_width * 30 + 20, 20))
    def collides(self):
        if self.current_piece:
            for row in range(4):
                for col in range(4):
                    if self.current_piece.shape[row][col] != 0:
                        if self.current_piece.x + col < 0 or self.current_piece.x + col >= self.board_width or self.current_piece.y + row >= self.board_height or self.board[self.current_piece.y + row][self.current_piece.x + col] != 0:
                            return True
        return False
    def place_piece(self):
        if self.current_piece:
            for row in range(4):
                for col in range(4):
                    if self.current_piece.shape[row][col] != 0:
                        self.board[self.current_piece.y + row][self.current_piece.x + col] = self.current_piece.shape[row][col]
    def remove_completed_lines(self):
        lines_removed = 0
        for row in range(self.board_height):
            if all(self.board[row]):
                del self.board[row]
                self.board.insert(0, [0] * self.board_width)
                lines_removed += 1
        self.score += lines_removed * 100
    def spawn_piece(self):
        self.current_piece = self.next_piece
        self.next_piece = Piece()
        if self.collides():
            self.current_piece = None
            self.next_piece = None
            self.score = 0

開始画面から始まらずに一行にブロックが落ちてこないゲームの出来上がりですね。

PygameとTetrisという前者はPythonでゲームを作る上でよく使われるライブラリで、後者はTetrisに特化したライブラリを扱っていますね。パっと見て大外れではないスクリプトは吐き出しているようですが、細かいデバッグは必要です。

ここで自分のプロンプトがおかしかったのか、そもそもChatDevの実力なのかがわからなかったので2度目に挑戦しました。(もしくはGPT-3.5なのがいけないのか?)

2回目の生成

今度は先ほどの5のコマンドを以下に変更しました。

python run.py –task “テトリス(画面上部からさまざまな形のブロックが画面下へと落下する。そのブロックを積み重ねていく中で横にそろった列を削除してポイントを加算していく。ブロックの形には十字、棒、L字、T時が存在する。)” –name “Tetris”

より具体度を挙げたプロンプトになっていると感じていただけるかと思います。

これで実行した結果、生成された「main.py」と「tetris.py」が以下の通りです。

'''
This is the main file of the Tetris game.
'''
import tkinter as tk
from tetris import Tetris
def main():
    root = tk.Tk()
    root.title("Tetris")
    tetris = Tetris(root)
    tetris.pack()
    root.mainloop()
if __name__ == "__main__":
    main()
'''
This file contains the Tetris class which represents the game logic and GUI.
'''
import tkinter as tk


class Tetris(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.master = master
        self.master.bind("<Key>", self.handle_keypress)
        self.board_width = 10
        self.board_height = 20
        self.board = [[0] * self.board_width for _ in range(self.board_height)]
        self.current_piece = None
        self.current_piece_coords = []
        self.score = 0
        self.create_widgets()
        self.start_game()
    def create_widgets(self):
        self.canvas = tk.Canvas(self, width=300, height=600, bg="white")
        self.canvas.pack()
        self.score_label = tk.Label(self, text="Score: 0")
        self.score_label.pack()
    def start_game(self):
        self.score = 0
        self.update_score()
        self.board = [[0] * self.board_width for _ in range(self.board_height)]
        self.current_piece = None
        self.current_piece_coords = []
        self.draw_board()
        self.draw_current_piece()
    def draw_board(self):
        self.canvas.delete("all")
        for row in range(self.board_height):
            for col in range(self.board_width):
                if self.board[row][col] != 0:
                    x1 = col * 30
                    y1 = row * 30
                    x2 = x1 + 30
                    y2 = y1 + 30
                    self.canvas.create_rectangle(x1, y1, x2, y2, fill="blue")
    def draw_current_piece(self):
        for coord in self.current_piece_coords:
            row, col = coord
            x1 = col * 30
            y1 = row * 30
            x2 = x1 + 30
            y2 = y1 + 30
            self.canvas.create_rectangle(x1, y1, x2, y2, fill="red")
    def update_score(self):
        self.score_label.config(text="Score: {}".format(self.score))
    def handle_keypress(self, event):
        if event.keysym == "Left":
            self.move_piece_left()
        elif event.keysym == "Right":
            self.move_piece_right()
        elif event.keysym == "Down":
            self.move_piece_down()
        elif event.keysym == "Up":
            self.rotate_piece()
    def move_piece_left(self):
        if self.current_piece is not None:
            new_coords = [(row, col - 1) for row, col in self.current_piece_coords]
            if self.is_valid_move(new_coords):
                self.clear_current_piece()
                self.current_piece_coords = new_coords
                self.draw_current_piece()
    def move_piece_right(self):
        if self.current_piece is not None:
            new_coords = [(row, col + 1) for row, col in self.current_piece_coords]
            if self.is_valid_move(new_coords):
                self.clear_current_piece()
                self.current_piece_coords = new_coords
                self.draw_current_piece()
    def move_piece_down(self):
        if self.current_piece is not None:
            new_coords = [(row + 1, col) for row, col in self.current_piece_coords]
            if self.is_valid_move(new_coords):
                self.clear_current_piece()
                self.current_piece_coords = new_coords
                self.draw_current_piece()
            else:
                self.place_piece()
                self.check_full_rows()
                self.spawn_new_piece()
    def rotate_piece(self):
        if self.current_piece is not None:
            new_coords = []
            for row, col in self.current_piece_coords:
                new_col = self.current_piece_coords[0][1] + self.current_piece_coords[0][0] - row
                new_row = self.current_piece_coords[0][0] - self.current_piece_coords[0][1] + col
                new_coords.append((new_row, new_col))
            if self.is_valid_move(new_coords):
                self.clear_current_piece()
                self.current_piece_coords = new_coords
                self.draw_current_piece()
    def is_valid_move(self, coords):
        for row, col in coords:
            if (
                row < 0
                or row >= self.board_height
                or col < 0
                or col >= self.board_width
                or self.board[row][col] != 0
            ):
                return False
        return True
    def clear_current_piece(self):
        for row, col in self.current_piece_coords:
            self.board[row][col] = 0
    def place_piece(self):
        for row, col in self.current_piece_coords:
            self.board[row][col] = 1
    def check_full_rows(self):
        full_rows = []
        for row in range(self.board_height):
            if all(cell != 0 for cell in self.board[row]):
                full_rows.append(row)
        for row in full_rows:
            self.board.pop(row)
            self.board.insert(0, [0] * self.board_width)
            self.score += 1
        self.update_score()
    def spawn_new_piece(self):
        self.current_piece = [
            [(0, 0), (0, 1), (0, 2), (0, 3)],  # I piece
            [(0, 0), (0, 1), (0, 2), (1, 2)],  # L piece
            [(0, 0), (0, 1), (0, 2), (1, 0)],  # J piece
            [(0, 0), (0, 1), (1, 1), (1, 0)],  # O piece
            [(0, 0), (0, 1), (1, 1), (0, 2)],  # S piece
            [(0, 1), (0, 2), (1, 0), (1, 1)],  # Z piece
            [(0, 0), (0, 1), (0, 2), (1, 1)],  # T piece
        ]
        self.current_piece_coords = [(0, self.board_width // 2 - 2)]
        self.draw_current_piece()

またしてもこれでは画面が動かないゲームが完成してしまいました…

ここで、1回目との違いを比べてみましょう!

1. GUIライブラリの変更

1回目のスクリプト: pygameを使用していました。

2回目のスクリプト: tkinterを使用しています。

2. ゲームロジックとGUIの組み合わせ

1回目のスクリプト: tetris.pyはゲームロジックを含んでいましたが、GUIの描画はmain.pyに依存していました。

2回目のスクリプト: TetrisクラスはtkinterのFrameクラスを継承し、ゲームロジックとGUIの両方を含んでいます。

3. ゲームの開始

1回目のスクリプト: ゲームはmain()関数内で開始され、ゲームループはpygameのイベントを使用していました。

2回目のスクリプト: start_gameメソッドを使用してゲームを開始し、tkinterのmainloopを使用しています。

4. ピースの操作と描画

1回目のスクリプト: ピースの操作と描画はそれぞれ異なるメソッドで行われていました。

2回目のスクリプト: ピースの操作と描画は統合され、tkinterのキャンバスとイベントハンドラを使用しています。

5. スコアの更新

1回目のスクリプト: スコアはpygameのフォントを使用して描画されていました。

2回目のスクリプト: スコアはtkinterのラベルウィジェットを使用して更新されています。

特に改善された点

ゲームの更新と描画のロジックが集約されていて、コードの読みやすさと保守性が上がった。

要するに…

様々な役割を持った人たち、例えばテスターがテストをしてくれているはずなのになんで動かないゲームができてしまうのかと考えたときに、構文的なエラーはなくしてくれるもののゲームとして正しいものかというところまで判断できないというのが一つの結果であると思います。

また、プロンプトの具体性が上がると生成物の精度も上がっていました。どんな事をしたいのかより明確化することがLLMを扱う上でのキーポイントと言われていますが、その結果が出たと言えると思います。

どんな使い方が良いか?

2回目の2つのスクリプトをChatGPT(GPT-4 )で対話しながらデバッグしてみたところ、10回未満のやり取りでゲームとして機能するテトリスを完成させることができました。

そうすると「ChatDevでたたき台を作ってもらってから、自分でデバッグする方法」と「初めからChatGPTで対話しながら実装する方法」とどちらが効率的かという結論をつけることが難しいと感じました。

本人の技量や作りたいものに左右されてしまうのが現状ですが、実装力があまりない人にとってはあまり変わらないと思います。

しかし、作りたいものがあまり定まっていない場合はそのたたき台を作ってもらうという使い方があるかもしれません。

おまけ

以下に修正した「main.py」と「tetris.py」を共有するので、生成されたものとゲームとして完成しているものの違いについて検討してみるのも面白いと思います。

ログを見て生成過程をみるのも面白いですよ!

'''
This is the main file of the Tetris game.
'''
import tkinter as tk
from tetris import Tetris
def main():
    root = tk.Tk()
    root.title("Tetris")
    tetris = Tetris(root)
    tetris.pack()
    root.mainloop()
if __name__ == "__main__":
    main()
import tkinter as tk
import random


class Tetris(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.master = master
        self.master.bind("<Key>", self.handle_keypress)
        self.board_width = 10
        self.board_height = 20
        self.board = [[0] * self.board_width for _ in range(self.board_height)]
        self.current_piece = None
        self.current_piece_coords = []
        self.score = 0
        self.create_widgets()
        self.start_game()


    def create_widgets(self):
        self.canvas = tk.Canvas(self, width=300, height=600, bg="white")
        self.canvas.pack()
        self.score_label = tk.Label(self, text="Score: 0")
        self.score_label.pack()


    def start_game(self):
        self.score = 0
        self.update_score()
        self.board = [[0] * self.board_width for _ in range(self.board_height)]
        self.current_piece = None
        self.current_piece_coords = []
        self.spawn_new_piece()
        self.draw_board()
        self.draw_current_piece()
        self.game_loop()


    def game_loop(self):
        try:
            self.move_piece_down()
        finally:
            self.master.after(500, self.game_loop)


    def draw_board(self):
        self.canvas.delete("all")
        for row in range(self.board_height):
            for col in range(self.board_width):
                if self.board[row][col] != 0:
                    x1 = col * 30
                    y1 = row * 30
                    x2 = x1 + 30
                    y2 = y1 + 30
                    self.canvas.create_rectangle(x1, y1, x2, y2, fill="blue")


    def draw_current_piece(self):
        for coord in self.current_piece_coords:
            row, col = coord
            x1 = col * 30
            y1 = row * 30
            x2 = x1 + 30
            y2 = y1 + 30
            self.canvas.create_rectangle(x1, y1, x2, y2, fill="red")


    def update_score(self):
        self.score_label.config(text="Score: {}".format(self.score))


    def handle_keypress(self, event):
        if event.keysym == "Left":
            self.move_piece_left()
        elif event.keysym == "Right":
            self.move_piece_right()
        elif event.keysym == "Down":
            self.move_piece_down()
        elif event.keysym == "Up":
            self.rotate_piece()
        self.draw_board()
        self.draw_current_piece()


    def move_piece_left(self):
        if self.current_piece is not None:
            new_coords = [(row, col - 1) for row, col in self.current_piece_coords]
            if self.is_valid_move(new_coords):
                self.clear_current_piece()
                self.current_piece_coords = new_coords
                self.draw_current_piece()


    def move_piece_right(self):
        if self.current_piece is not None:
            new_coords = [(row, col + 1) for row, col in self.current_piece_coords]
            if self.is_valid_move(new_coords):
                self.clear_current_piece()
                self.current_piece_coords = new_coords
                self.draw_current_piece()


    def move_piece_down(self):
        if self.current_piece is not None:
            new_coords = [(row + 1, col) for row, col in self.current_piece_coords]
            if self.is_valid_move(new_coords):
                self.clear_current_piece()
                self.draw_board()
                self.current_piece_coords = new_coords
                self.draw_current_piece()
            else:
                self.place_piece()
                self.check_full_rows()
                self.spawn_new_piece()


    def rotate_piece(self):
        if self.current_piece is not None:
          center_row, center_col = self.current_piece_coords[0]
          new_coords = [(center_row + col - center_col, center_col - row + center_row) for row, col in self.current_piece_coords]
          if self.is_valid_move(new_coords):
            self.clear_current_piece()
            self.current_piece_coords = new_coords
            self.draw_current_piece()


    def is_valid_move(self, coords):
        for row, col in coords:
            if (
                row < 0
                or row >= self.board_height
                or col < 0
                or col >= self.board_width
                or (self.board[row][col] != 0 and (row, col) not in self.current_piece_coords)
            ):
                return False
        return True


    def clear_current_piece(self):
        for row, col in self.current_piece_coords:
            self.board[row][col] = 0


    def place_piece(self):
        for row, col in self.current_piece_coords:
            self.board[row][col] = 1


    def check_full_rows(self):
        full_rows = []
        for row in range(self.board_height):
            if all(cell != 0 for cell in self.board[row]):
                full_rows.append(row)
        for row in full_rows:
            self.board.pop(row)
            self.board.insert(0, [0] * self.board_width)
            self.score += 1
        self.update_score()
        self.draw_board()


    def spawn_new_piece(self):
        pieces = [
            [(0, 0), (0, 1), (0, 2), (0, 3)],
            [(0, 0), (0, 1), (0, 2), (1, 2)],
            [(0, 0), (0, 1), (0, 2), (1, 0)],
            [(0, 0), (0, 1), (1, 1), (1, 0)],
            [(0, 0), (0, 1), (1, 1), (0, 2)],
            [(0, 1), (0, 2), (1, 0), (1, 1)],
            [(0, 0), (0, 1), (0, 2), (1, 1)],
        ]
        self.current_piece = random.choice(pieces)
        self.current_piece_coords = [(coord[0], coord[1] + self.board_width // 2 - 2) for coord in self.current_piece]
        self.draw_current_piece()

まとめ

以上、「ChatDevを利用してテトリスを生成させた結果」についてでした。話題になっているものについて自分で実際に手を動かしてみることで新しい知識に多く、また深く触れることができると思います。

読んでいただきありがとうございました!

メンバーを募集中!

当社ではお客様に向き合い、お客様の事業成功にコミットをできる仲間を募集しています。

正社員、インターンから副業まで、幅広く採用活動を進めております。プロジェクトマネージャーやシステムエンジニアとして当社のシステム開発業務に携わってみませんか?

当社と共に、業界トップを目指して挑戦したい方は未経験者から経験者まで広く募集しているので、ぜひエントリーをお待ちしています。

「システムを短納期かつ低予算で作成したい」
ローコード開発に興味がある
「煩雑なExcel管理から脱したい」
慣れ親しんだシステムを使って効率化したい
「大規模なシステム開発を行うためにPoCを行いたい」
「社内のDXを進めていきたい」
  • URLをコピーしました!

この記事を書いた人

株式会社ファンリピートのアバター

株式会社ファンリピート

FRnoteは株式会社ファンリピートのメンバーによって運営されている社内ブログです。ノーコード・ローコードの技術ブログを始めとして、最新のIT技術、業務で役立つノウハウなど様々なトピックをまとめています。


目次
閉じる