【IBM Cloud】ChatGPTを利用してゲームアプリケーションを作成し、IBM Cloud Kubernetes Servicesに載せてみた

## はじめに
今回はChatGPTを使って簡単なゲームアプリケーションを作成し、IBM Cloud Kuberenetes Services（IKS）に載せて動かしてみる検証を行いました。全体的な流れを記述するとともに、問題なくアプリケーションがデプロイされたことを確認することを目的として作業しました。
## 前提条件
・今回の内容にはIBM Cloudの[アカウント](https://cloud.ibm.com/docs/account?topic=account-accounts)（無料利用可能）が前提となっています。
・この記事でのコマンドは全てMacOS向けとなっています。
## ChatGPTを使ってコードを書いてもらう
まず、ChatGPTを使ってゲームアプリケーションを作成してもらいます。注意点としては、すでにご存知の方も多いかと思いますが、ChatGPTは間違った情報も堂々と正解のように教えてくれるケースも少なくないので、現時点ではその情報の見分けが必要です。

簡単なゲームアプリを作成するに、今回はpygameを選びました。pygameは、ビデオゲームを製作するために設計されたクロスプラットフォームのPythonモジュール集です。使用する言語はもちろんPythonとなります。
pygameが初めての方は、以下のコマンドでInstallを実施します。
```shell:pygame install
pip install pygame
```

以下、pygameでゲームを作成するために早速ChatGPTに質問を投げます。

```shell:ChatGPTにゲームアプリケーションの作成方法を聞く
pygameコードで何かできるものはない？
```
```shell:その答え
pygameを使用して実現できる様々な機能やゲームのアイデアがあります。以下にいくつかの例を挙げます:

1. スペースインベーダーゲーム: プレイヤーシップを操作して、降りてくる敵の攻撃を避けながら撃墜するゲームです。
2. パズルゲーム: ピースを動かして絵やパターンを完成させるゲームです。例えば、スライディングパズルやジグソーパズルなどがあります。
3. プラットフォームゲーム: ジャンプや障害物回避をしながらレベルを進めるゲームです。例えば、マリオやソニックのようなゲームがあります。
4. シューティングゲーム: プレイヤーが敵を撃破するために武器を使用するゲームです。例えば、縦スクロールシューティングや横スクロールシューティングなどがあります。
5. レースゲーム: プレイヤーが車やバイクを操作して他の競技者と競い合うゲームです。例えば、カートゲームやレーシングシミュレータなどがあります。
```
と、何種類かのゲームを教えてくれます。
私は1番のスペースインベーダーゲームが気になったので、そのコードを書いてもらうように要請しました。

```shell:スペースインベーダーゲームのコードの要請
1番のスペースインベーダーゲームのコマンドを教えて
```
```shell:その答え
以下に、基本的なスペースインベーダーゲームのコードを示します。このコードはpygameを使用してスペースインベーダーゲームを作成するためのものです。以下のコードを実行すると、ゲームウィンドウが表示され、プレイヤーシップと敵のスペースインベーダーが表示されます。プレイヤーシップは矢印キーで移動し、スペースキーで弾を発射することができます。

import pygame
import random

# 初期化
pygame.init()

# ゲームウィンドウの作成
width = 800
height = 600
screen = pygame.display.set_mode((width, height))
pygame.display.set_caption("Space Invaders")

# カラー定義
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

# プレイヤーシップの作成
player_width = 50
player_height = 50
player_x = width // 2 - player_width // 2
player_y = height - player_height - 10
player_speed = 5

player = pygame.Rect(player_x, player_y, player_width, player_height)

# スペースインベーダーの作成
invader_width = 40
invader_height = 40
invader_speed = 2

invaders = []
for row in range(5):
    for col in range(10):
        invader_x = col * (invader_width + 10) + 50
        invader_y = row * (invader_height + 10) + 50
        invader = pygame.Rect(invader_x, invader_y, invader_width, invader_height)
        invaders.append(invader)

# 弾の作成
bullet_width = 5
bullet_height = 10
bullet_speed = 5

bullet = None
bullet_state = "ready"  # "ready" or "fired"

# ゲームループ
running = True
clock = pygame.time.Clock()

while running:
    clock.tick(60)
    
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE and bullet_state == "ready":
                bullet_state = "fired"
                bullet_x = player.x + player.width // 2 - bullet_width // 2
                bullet_y = player.y
                bullet = pygame.Rect(bullet_x, bullet_y, bullet_width, bullet_height)
    
    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT] and player.x > 0:
        player.x -= player_speed
    if keys[pygame.K_RIGHT] and player.x < width - player_width:
        player.x += player_speed
    
    if bullet_state == "fired":
        bullet.y -= bullet_speed
        if bullet.y < 0:
            bullet_state = "ready"
    
    screen.fill(BLACK)
    
    pygame.draw.rect(screen, WHITE, player)
    
    for invader in invaders:
        pygame.draw.rect(screen, WHITE, invader)
    
    if bullet_state == "fired":
        pygame.draw.rect(screen, WHITE, bullet)
    
    pygame.display.update()

# 終了
pygame

```

上記のコードに加えて、銃を連打するコードを追加したり、敵の色を変更するなど、カスタマイズのためのコードも要請することで、コードを書けない人でも簡単にゲームを作成することができます。
色々学習させた後の最終盤コードが以下となります。
```shell:スペースインベーダーゲーム最終盤コード
import pygame
import random

# 初期化
pygame.init()

# ゲームウィンドウの作成
width = 800
height = 600
screen = pygame.display.set_mode((width, height))
pygame.display.set_caption("Space Invaders")

# カラー定義
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (128, 128, 128)
RED = (255, 0, 0)

# プレイヤーシップの作成
player_width = 50
player_height = 50
player_x = width // 2 - player_width // 2
player_y = height - player_height - 10
player_speed = 5

player = pygame.Rect(player_x, player_y, player_width, player_height)

# スペースインベーダーの作成
invader_width = 40
invader_height = 40
invader_speed = 2
invader_direction = 1

invaders = []
for row in range(5):
    for col in range(10):
        invader_x = col * (invader_width + 10) + 50
        invader_y = row * (invader_height + 10) + 50
        invader = pygame.Rect(invader_x, invader_y, invader_width, invader_height)
        invaders.append(invader)

# 弾の作成
bullet_width = 5
bullet_height = 10
bullet_speed = 5

bullets = []

# 敵の攻撃
enemy_bullet_width = 3
enemy_bullet_height = 10
enemy_bullet_speed = 3

enemy_bullets = []

# スコア
score = 0
font = pygame.font.Font(None, 36)

# ゲームオーバーメッセージ
game_over_font = pygame.font.Font(None, 72)

# ゲームループ
running = True
clock = pygame.time.Clock()

def draw_score():
    score_text = font.render("Score: " + str(score), True, WHITE)
    screen.blit(score_text, (10, 10))

def draw_game_over():
    game_over_text = game_over_font.render("Game Over", True, WHITE)
    game_over_rect = game_over_text.get_rect(center=(width // 2, height // 2))
    screen.blit(game_over_text, game_over_rect)

while running:
    clock.tick(60)
    
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                bullet_x = player.x + player.width // 2 - bullet_width // 2
                bullet_y = player.y
                bullet = pygame.Rect(bullet_x, bullet_y, bullet_width, bullet_height)
                bullets.append(bullet)
    
    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT] and player.x > 0:
        player.x -= player_speed
    if keys[pygame.K_RIGHT] and player.x < width - player_width:
        player.x += player_speed
    
    for bullet in bullets:
        bullet.y -= bullet_speed
        if bullet.y < 0:
            bullets.remove(bullet)
    
    for invader in invaders:
        invader.x += invader_speed * invader_direction
        if invader.x < 0 or invader.x > width - invader_width:
            invader_direction *= -1
            for invader in invaders:
                invader.y += 20
        
        if invader.colliderect(player):
            running = False
        
        if random.randint(0, 1000) < 5 and len(enemy_bullets) < 5:
            enemy_bullet_x = invader.x + invader.width // 2 - enemy_bullet_width // 2
            enemy_bullet_y = invader.y + invader.height
            enemy_bullet = pygame.Rect(enemy_bullet_x, enemy_bullet_y, enemy_bullet_width, enemy_bullet_height)
            enemy_bullets.append(enemy_bullet)
    
    for bullet in bullets:
        for enemy_bullet in enemy_bullets:
            if bullet.colliderect(enemy_bullet):
                bullets.remove(bullet)
                enemy_bullets.remove(enemy_bullet)
    
    for enemy_bullet in enemy_bullets:
        enemy_bullet.y += enemy_bullet_speed
        if enemy_bullet.colliderect(player):
            running = False
        if enemy_bullet.y > height:
            enemy_bullets.remove(enemy_bullet)
    
    for bullet in bullets:
        for invader in invaders:
            if bullet.colliderect(invader):
                invaders.remove(invader)
                bullets.remove(bullet)
                score += 10
    
    screen.fill(BLACK)
    
    pygame.draw.rect(screen, WHITE, player)
    
    for invader in invaders:
        pygame.draw.rect(screen, GRAY, invader)  # 灰色で描画
    
    for bullet in bullets:
        pygame.draw.rect(screen, WHITE, bullet)
    
    for enemy_bullet in enemy_bullets:
        pygame.draw.rect(screen, RED, enemy_bullet)  # 赤色で描画
    
    draw_score()
    
    if len(invaders) == 0:
        draw_game_over()
    
    pygame.display.update()

# 終了
pygame.quit()

```
注意すべき点としては、上記の通り間違ったコードなども何回も与えられたため、結局細かい部分の修正が必要でした。ChatGPTに投げる質問が悪かったかもしれませんが、間違った場合にはChatGPTに間違った旨と正しく修正したコードを再度要請することである程度対応することができました。

作成できたコードはコードエディターに入れて扱いましたが、今回はVisual Studio Code（VScode）を使用しました。作成されたコードを実行するために、右上の`プレーボタン`をクリックします。ゲーム画面が表示され、プレーすることが可能になります。
![スクリーンショット 2023-05-31 18.38.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2626032/dc29cdf8-4e5e-7d52-b93f-59e499f92026.png)

![画面収録 2023-05-31 18.42.12.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2626032/0b6d8201-a46f-e322-9472-989e5b6c16ed.gif)

## pygameをDockerfileに変換する
PythonのゲームをDockerイメージに変換するために、Dockerfileを作成する必要があります。
VSCodeでDockerfileを作成し、以下のコードを記載しておきます。
```shell:Dockerfileの作成
FROM python:3.11.3

WORKDIR /app

# ソースコードをコンテナ内の/appディレクトリにコピーする
COPY game.py .

# 必要なパッケージをインストールする
RUN pip install pygame

CMD ["python", "game.py"]

```
## Docker Hubに格納する
アプリケーションの作成が完了したら、次にはアプリケーションのDockerイメージを作成し、Docker Hubに格納します。
注意点としては、以下の作業はDockerfileがあるディレクトリに移動してコマンドを実行する必要があります。
```shell:Docker Hubにログイン
docker login
```
```shell:Docker Hubに格納
docker build -t game-test:latest .
```
```shell:DockerイメージのPush
docker tag space-invaders:latest <DockerHubのユーザ名>/space-invaders:latest
docker push <DockerHubのユーザ名>/space-invaders:latest
```
すると、Docker HubにDockerイメージが格納されたことが確認できます。
![スクリーンショット 2023-05-31 19.03.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2626032/bb238778-e775-b5a7-cfc5-1b93824e78f6.png)



## IKSクラスター上でデプロイ
アプリケーションを載せるために、IKSのクラスターを作成します。クラスター作成の具体的な流れは過去に作成した[こちらの記事](https://qiita.com/Sungyun_Moon/items/75933a4516f56175fe8a)に書かれています。過去記事にはClassicクラスターを作成しましたが、今回はVPCクラスターを作成しています。

ターミナルにて、IBM Cloudにログインします。ログインはCLIを使用したワンタイム・パスコードを利用すると便利です。
右上のプロフィールアイコンをクリックし、`CLIとAPIにログイン`をクリックしますと、以下のログインCLIが表示されます。
IBM Cloud CLIをコピーしコマンド入力にてログインを行います。
![スクリーンショット 2023-01-27 14.52.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2626032/85389613-4b70-eda8-43d4-a0da1211064c.png)
![スクリーンショット 2023-01-27 14.52.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2626032/04c3ad3e-31da-6abc-804c-506a5536429d.png)

その後、以下のコマンドを使ってクラスターを指定します。
今回アプリケーションをデプロイするクラスターを指定します。
Kubernetesの環境を設定するには、exportコマンドが必要です。exportコマンドは、`ibmcloud ks cluster config`コマンドを入力して取得します。
なお、クラスターIDはクラスター画面に表示されています。
```shell:クラスター環境指定
ibmcloud ks cluster config --cluster <cluster-id>
```
```shell:クラスター環境指定の結果
<cluster-id> のコンテキストを現在の kubeconfig ファイルに追加しました。
これで、クラスターに対して「kubectl」コマンドを実行できます。 例えば、「kubectl get nodes」を実行します。
クラスターに初めてアクセスする場合、RBAC の同期中に「kubectl」コマンドが数秒間失敗することがあります。
```
![スクリーンショット 2023-01-27 15.05.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2626032/b737f13f-2b5f-980e-a4dd-57c223ac7d4b.png)

## IKSリソースの作成
Kubernetesリソースの作成: アプリケーションを実行するために必要なKubernetesリソースを定義します。一般的なリソースには、Deployment、Service、Ingressなどがあります。Deploymentはアプリケーションのコンテナを管理し、Serviceはアクセスを提供し、Ingressは外部からのトラフィックをルーティングする役割を持ちます。
今回はDeploymentとServiceのみ使用しています。

VSCodeにてDeploymentファイルとServiceファイルをyaml形式で作成します。
```shell:Deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: game-test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: game-test
  template:
    metadata:
      labels:
        app: game-test
    spec:
      containers:
        - name: game-test
          image: sungyunmoon/space-invaders:latest
          # resources:
          #   requests:
          #     cpu: "0.5"
          #     memory: "512Mi"
          #   limits:
          #     cpu: "1"
          #     memory: "1Gi"
          ports:
            - containerPort: 8080
```
```shell:Service.yaml
# service.yaml

apiVersion: v1
kind: Service
metadata:
  name: game-test
spec:
  selector:
    app: game-test
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
```
yamlファイルの作成が完了したら、ターミナルにて以下のコマンドを入力し、IKS上にデプロイを行います。
```shell:yamlをIKS上にデプロイ
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
```

その後、以下コマンドにてデプロイを確認します。
```shell:デプロイの確認
kubectl get pods
kubectl get services
```
```shell:デプロイの結果
NAME                         READY   STATUS    RESTARTS     AGE
game-test-5896d6c7b5-8n7k5   1/1     Running   1 (5s ago)   11s
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
game-test    ClusterIP   172.21.102.241   <none>        80/TCP    2m5s
kubernetes   ClusterIP   172.21.0.1       <none>        443/TCP   3m7s
```
問題なくIKS上にデプロイされたことがわかります。

## 課題
IKSにデプロイした数分後、ターミナルで確認したところ、DeploymentのStatusに`CrashLoopBackOff`というエラーが発生していました。念の為IKSのコンソールで確認しても赤色で`Back-off restarting failed container`というエラーが表示されています。
```shell:デプロイの結果
sungyunmoon@at-Mac-Pro python % kubectl get pods
kubectl get services

NAME                         READY   STATUS             RESTARTS       AGE
game-test-5896d6c7b5-8n7k5   0/1     CrashLoopBackOff   6 (2m6s ago)   8m22s
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
game-test    ClusterIP   172.21.102.241   <none>        80/TCP    8m22s
kubernetes   ClusterIP   172.21.0.1       <none>        443/TCP   9m24s
```
![スクリーンショット 2023-05-31 19.47.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2626032/7151c65c-3c73-48e8-95c0-efa9d9accf82.png)

あくまでも推測ですが、作成したアプリケーションは一回きりの実行がされていて、継続実行ができないコードとなっているため、このようなエラーが発生しているのではないかと思います。
こちらの詳細はまだ勉強が必要な部分であるため、解決でき次第追記できればと思います。

もしおわかりになる方は、コメントにてご指摘いただけますと幸いです。

## 最後に
今回はChatGPTでアプリケーションを作成し、IKS上にデプロイする流れを検証しまとめてみました。

私自身はコーディングは全くできない初心者で、IBM Cloudリソースの検証にはいつも事前に作られているサンプルアプリを使うことがほとんどでしたが、ChatGPTを利用することによって、初心者でも自分のアプリケーションを作成して動かしてみることができるのは楽しい作業となりました。

また、IBM Cloud上にはCode Engineをはじめとするクラウドネイティブ向けの数多いサービスがありますが、今回手作業でアプリケーションをデプロイしてみることによって、Code Engineなどのサービスがどれほど楽で有効なサービスなのかも改めて感じました。

## 参考
- [IBM Cloud Docs - Kubernetes Serivces](https://cloud.ibm.com/docs/containers?topic=containers-getting-started)
- [ChatGPT](https://chat.openai.com/?model=gpt-4)