AIに遊んでもらうゲームをAIに作ってもらう#2


前回の記事では「すごろく」を実現するAPIを簡単に構築しました。

今回はゲーム性を上げるために、以下の機能追加をします。

  • 進む・戻る機能の追加

  • 複数ユーザーで遊べるように機能の追加

  • 表示用(GUI用)のデータ取得機能の追加

機能追加の前に、一部の数値を定数として定義して下記のコードとしていきます。また、初回プレイ時の挙動も変更しています。

from flask import Flask, request, jsonify
import random

app = Flask(__name__)

# ユーザーの位置を管理するための簡易的なデータベース (本番環境では実際のデータベースを使用)
user_positions = {}

# 各マスの効果
effects = {
    1: "スタートです!",
    2: "数学に関する面白い話をする",
    3: "英語に関する面白い話をする",
    4: "化学に関する面白い話をする",
    5: "物理に関する面白い話をする",
    6: "地理に関する面白い話をする",
    7: "ゴールしました!"
}

# サイコロの最大値を変数で定義
DICE_MAX_VALUE = 3
# ゴール位置をeffectsの長さから算出
GOAL_POSITION = len(effects)

@app.route('/sugoroku', methods=['GET'])
def play_sugoroku_get_updated():
    name = request.args.get("name")
    
    # ユーザーの初回プレイかどうかをチェック
    if name not in user_positions:
        user_positions[name] = 1  # スタート位置
        response = {
            "name": name,
            "dice": None,
            "position": 1,
            "effect": effects[1]
        }
        return jsonify(response), 200
    
    # サイコロを振る
    dice = random.randint(1, DICE_MAX_VALUE)
    
    # ユーザーの位置を更新
    user_positions[name] += dice
    
    # ゴールを超えた場合の処理
    if user_positions[name] > GOAL_POSITION:
        del user_positions[name]
    
    position = user_positions.get(name, GOAL_POSITION)  # ゴールした場合はGOAL_POSITIONをデフォルトとして返す
    effect = effects[position]
    
    response = {
        "name": name,
        "dice": dice,
        "position": position,
        "effect": effect
    }
    
    return jsonify(response), 200

if __name__ == '__main__':
    app.run(debug=True,host='0.0.0.0',port=8888)

コード部分は一部省略しますので、実行する際は上記のコードを参考に適宜必要部分を追加してください。

進む・戻る機能の追加

進む・戻る機能の追加のために以下のプロンプトでGPT-4に変更をしてもらいます。

各マスの効果に「1マス戻る」や「2マス進む」などの効果をつけれるようにeffectsの構造とロジックを変更してください。

   position = user_positions.get(name, GOAL_POSITION)  # ゴールした場合はGOAL_POSITIONをデフォルトとして返す
    
    # 効果のアクションを適用
    user_positions[name] += effects[position]["action"]
    
    # アクションの適用後の位置を再取得
    position = user_positions.get(name, GOAL_POSITION)
    
    effect_message = effects[position]["message"]
    
    response = {
        "name": name,
        "dice": dice,
        "position": position,
        "effect": effect_message
    }

このコードでは、戻ったり進んだりした先のeffectが応答されしまって、戻ったり進んだりしたことが読み取りにくいので変更します。

アクションの効果でポジションが変更された場合のeffect_messageは、アクションの効果でポジションが変更される前のeffect_messageにしてください。

   position = user_positions.get(name, GOAL_POSITION)  # ゴールした場合はGOAL_POSITIONをデフォルトとして返す
    
    # アクションの効果でポジションが変更される前のeffect_messageを取得
    effect_message = effects[position]["message"]
    
    # 効果のアクションを適用
    user_positions[name] += effects[position]["action"]
    
    # アクションの適用後の位置を再取得
    position = user_positions.get(name, GOAL_POSITION)
    
    response = {
        "name": name,
        "dice": dice,
        "position": position,
        "effect": effect_message
    }

ここでコードを実行してみると、ゴールした際に【user_positions[name] += effects[position]["action"]】の部分でエラーになるので修正してもらいます。

def play_sugoroku_get_updated():
    name = request.args.get("name")
    
    # ユーザーの初回プレイかどうかをチェック
    if name not in user_positions:
        user_positions[name] = 1  # スタート位置
        response = {
            "name": name,
            "dice": None,
            "position": 1,
            "effect": effects[1]["message"]
        }
        return jsonify(response), 200
    
    # サイコロを振る
    dice = random.randint(1, DICE_MAX_VALUE)
    
    # ユーザーの位置を更新
    user_positions[name] += dice
    
    # ゴールを超えた場合の処理
    if user_positions[name] > GOAL_POSITION:
        position = GOAL_POSITION
        del user_positions[name]
    else:
        position = user_positions[name]
    
    # アクションの効果でポジションが変更される前のeffect_messageを取得
    effect_message = effects[position]["message"]
    
    # 効果のアクションを適用
    if name in user_positions:  # ユーザーの位置が存在する場合のみアクションを適用
        user_positions[name] += effects[position]["action"]
    
    # アクションの適用後の位置を再取得
    position = user_positions.get(name, GOAL_POSITION)
    
    response = {
        "name": name,
        "dice": dice,
        "position": position,
        "effect": effect_message
    }
    
    return jsonify(response), 200

今度は、進むマスの効果でゴールに入った際にゴール判定になりませんので修正します。

def play_sugoroku_get_updated():
    name = request.args.get("name")
    
    # ユーザーの初回プレイかどうかをチェック
    if name not in user_positions:
        user_positions[name] = 1  # スタート位置
        response = {
            "name": name,
            "dice": None,
            "position": 1,
            "effect": effects[1]["message"]
        }
        return jsonify(response), 200
    
    # サイコロを振る
    # dice = random.randint(1, DICE_MAX_VALUE)
    dice = 9
    
    # ユーザーの位置を更新
    user_positions[name] += dice
    
    # 効果のアクションを適用
    if user_positions[name] <= GOAL_POSITION:
        user_positions[name] += effects[user_positions[name]]["action"]
    
    # ゴールを超えた場合の処理
    if user_positions[name] >= GOAL_POSITION:
        user_positions[name] = GOAL_POSITION  # 位置をゴール位置に設定
        effect_message = effects[GOAL_POSITION]["message"]
        del user_positions[name]
    else:
        effect_message = effects[user_positions[name]]["message"]

    position = user_positions.get(name, GOAL_POSITION)
    
    response = {
        "name": name,
        "dice": dice,
        "position": position,
        "effect": effect_message
    }
    
    return jsonify(response), 200

これでひとまず動きそうなので、進む・戻る機能は実装完了とします。

複数ユーザーで遊べるように機能の追加

同じ要領でGPT-4と何度かやり取りをしながらコーディングを進めた結果が以下のコードです。
(何度かGPT-4が間違ってる!と思って手動で直そうと思いましたが、毎回人間側の理解が間違ってました…)

ゲーム開始を実装

from flask import Flask, request, jsonify
import random

app = Flask(__name__)

# ユーザーの位置を管理するための簡易的なデータベース (本番環境では実際のデータベースを使用)
user_positions = {}
current_player = None  # 現在サイコロを振るべきプレイヤー
game_start = False

# 各マスの効果
effects = {
    1: {"message": "スタートです!", "action": 0},
    2: {"message": "数学に関する面白い話をする", "action": 0},
    3: {"message": "1マス戻る", "action": -1},
    4: {"message": "化学に関する面白い話をする", "action": 0},
    5: {"message": "2マス進む", "action": 2},
    6: {"message": "地理に関する面白い話をする", "action": 0},
    7: {"message": "ゴールしました!", "action": 0}
}

# サイコロの最大値を変数で定義
DICE_MAX_VALUE = 3
# ゴール位置をeffectsの長さから算出
GOAL_POSITION = len(effects)

@app.route('/start', methods=['GET'])
def start_game():
    global game_start
    global current_player
    game_start = True
    current_player = current_player = list(user_positions.keys())[0] if user_positions else None
    return jsonify({"message": "Game started!", "current_player": current_player}), 200

@app.route('/sugoroku', methods=['GET'])
def play_sugoroku_get_updated():
    global current_player
    name = request.args.get("name")

    # ユーザーの初回プレイかどうかをチェック
    if name not in user_positions:
        if game_start:
            return jsonify({"error": "New players can't join in the middle of a game."}), 400
        else:
            user_positions[name] = 1  # スタート位置
            response = {
                "name": name,
                "dice": None,
                "position": 1,
                "effect": effects[1]["message"]
            }
            return jsonify(response), 200
        
    if name != current_player:
        return jsonify({"error": "It's not your turn!"}), 400

    # サイコロを振る
    dice = random.randint(1, DICE_MAX_VALUE)
    # dice = 9
    
    # ユーザーの位置を更新
    user_positions[name] += dice
    
    # 効果のアクションを適用
    if user_positions[name] <= GOAL_POSITION:
        user_positions[name] += effects[user_positions[name]]["action"]

    # 次のプレイヤーへターンを移動
    players = list(user_positions.keys())
    current_index = players.index(name)
    next_index = (current_index + 1) % len(players)
    current_player = players[next_index]
        
    # ゴールを超えた場合の処理
    if user_positions[name] >= GOAL_POSITION:
        user_positions[name] = GOAL_POSITION  # 位置をゴール位置に設定
        effect_message = effects[GOAL_POSITION]["message"]
        del user_positions[name]
    else:
        effect_message = effects[user_positions[name]]["message"]

    position = user_positions.get(name, GOAL_POSITION)
    
    response = {
        "name": name,
        "dice": dice,
        "position": position,
        "effect": effect_message
    }

    return jsonify(response), 200

if __name__ == '__main__':
    app.run(debug=True,host='0.0.0.0',port=8888)

初回アクセス後はゲームが始まるまではエラーが帰ってきます。
ゲーム開始は「/start」にアクセスすることで行えます。
ゲーム中は、自分のターンであれば「サイコロの出目」と「進んだ先の効果」が得られ、自分のターンでない場合はエラーが帰ってきます。

ゲーム終了を実装

from flask import Flask, request, jsonify
import random

app = Flask(__name__)

# ユーザーの位置とゴール状態を管理するための簡易的なデータベース
users = {}
current_player = None  # 現在サイコロを振るべきプレイヤー
game_start = False

# 各マスの効果
effects = {
    1: {"message": "スタートです!", "action": 0},
    2: {"message": "数学に関する面白い話をする", "action": 0},
    3: {"message": "1マス戻る", "action": -1},
    4: {"message": "化学に関する面白い話をする", "action": 0},
    5: {"message": "2マス進む", "action": 2},
    6: {"message": "地理に関する面白い話をする", "action": 0},
    7: {"message": "ゴールしました!", "action": 0}
}

# サイコロの最大値を変数で定義
DICE_MAX_VALUE = 3
# ゴール位置をeffectsの長さから算出
GOAL_POSITION = len(effects)

@app.route('/start', methods=['GET'])
def start_game():
    global game_start
    global current_player
    game_start = True
    current_player = list(users.keys())[0] if users else None
    return jsonify({"message": "Game started!", "current_player": current_player}), 200

@app.route('/end', methods=['GET'])
def end_game():
    global game_start
    global current_player
    users.clear()
    game_start = False
    current_player = None
    return jsonify({"message": "Game ended and reset!"}), 200

@app.route('/sugoroku', methods=['GET'])
def play_sugoroku_get_updated():
    global current_player
    global game_start
    name = request.args.get("name")

    if users.get(name) and users[name]["position"] == GOAL_POSITION:
        return jsonify({"error": "You've already reached the goal!"}), 400

    if name not in users:
        if game_start:
            return jsonify({"error": "New players can't join in the middle of a game."}), 400
        else:
            users[name] = {"position": 1, "finished": False}
            response = {
                "name": name,
                "dice": None,
                "position": 1,
                "effect": effects[1]["message"]
            }
            return jsonify(response), 200

    while current_player and users[current_player]["finished"]:
        players = list(users.keys())
        current_index = players.index(current_player)
        next_index = (current_index + 1) % len(players)
        current_player = players[next_index]

    if name != current_player:
        return jsonify({"error": "It's not your turn!"}), 400

    dice = random.randint(1, DICE_MAX_VALUE)
    users[name]["position"] += dice

    if users[name]["position"] <= GOAL_POSITION:
        users[name]["position"] += effects[users[name]["position"]]["action"]

    players = list(users.keys())
    current_index = players.index(name)
    next_index = (current_index + 1) % len(players)
    current_player = players[next_index]

    if users[name]["position"] >= GOAL_POSITION:
        users[name]["position"] = GOAL_POSITION
        users[name]["finished"] = True
        effect_message = effects[GOAL_POSITION]["message"]
    else:
        effect_message = effects[users[name]["position"]]["message"]

    position = users[name]["position"]

    response = {
        "name": name,
        "dice": dice,
        "position": position,
        "effect": effect_message
    }

    return jsonify(response), 200

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=8888)

全員がゴールするまで待つ(エラーが帰ってくる)ようになりました。
ゲームをリスタートするには「/end」へアクセスする必要があります。

全員がゴールしたら自動でリスタートしても良かったのですが、複数のAIキャラクターがアクセスする場合を考えると、どこかで人の手で介入できる余地があったほうが安全だと考えてこのような設計にしています。

表示用(GUI用)のデータ取得機能の追加

usersなどの変数を応答するだけなので、以下のメソッドを追加します。

@app.route('/status', methods=['GET'])
def get_status():
    return jsonify({"game_start": game_start, "users": users, "current_player":current_player}), 200

というか、この関数、デバッグ用に早い段階で作ればよかったと後悔しました…

リファクタリング

最後にGPT-4にリファクタリングしてもらって、以下のコードで一旦完成とします。

from flask import Flask, request, jsonify
import random

app = Flask(__name__)

# ユーザーの位置とゴール状態を管理するための簡易的なデータベース
users = {}
current_player = None  # 現在サイコロを振るべきプレイヤー
game_start = False

# 各マスの効果
SPACES = {
    1: {"message": "スタートです!", "action": 0},
    2: {"message": "数学に関する面白い話をする", "action": 0},
    3: {"message": "1マス戻る", "action": -1},
    4: {"message": "化学に関する面白い話をする", "action": 0},
    5: {"message": "2マス進む", "action": 2},
    6: {"message": "地理に関する面白い話をする", "action": 0},
    7: {"message": "ゴールしました!", "action": 0}
}

# サイコロの最大値を変数で定義
DICE_MAX_VALUE = 3
# ゴール位置をSPACESの長さから算出
GOAL_POSITION = len(SPACES)

def move_to_next_player():
    global current_player
    players = list(users.keys())
    current_index = players.index(current_player)
    next_index = (current_index + 1) % len(players)
    current_player = players[next_index]

@app.route('/status', methods=['GET'])
def get_status():
    return jsonify({"game_start": game_start, "users": users, "current_player":current_player}), 200

@app.route('/start', methods=['GET'])
def start_game():
    global game_start
    global current_player
    game_start = True
    current_player = list(users.keys())[0] if users else None
    return jsonify({"message": "Game started!", "current_player": current_player}), 200

@app.route('/end', methods=['GET'])
def end_game():
    global game_start
    global current_player
    users.clear()
    game_start = False
    current_player = None
    return jsonify({"message": "Game ended and reset!"}), 200

@app.route('/sugoroku', methods=['GET'])
def play_sugoroku_get_updated():
    global current_player
    global game_start
    name = request.args.get("name")

    if users.get(name) and users[name]["position"] == GOAL_POSITION:
        return jsonify({"error": "You've already reached the goal!"}), 400

    if name not in users:
        if game_start:
            return jsonify({"error": "New players can't join in the middle of a game."}), 400
        else:
            users[name] = {"position": 1, "finished": False}
            response = {
                "name": name,
                "dice": None,
                "position": 1,
                "effect": SPACES[1]["message"]
            }
            return jsonify(response), 200

    while current_player and users[current_player]["finished"]:
        move_to_next_player()

    if name != current_player:
        return jsonify({"error": "It's not your turn!"}), 400

    dice = random.randint(1, DICE_MAX_VALUE)
    users[name]["position"] += dice

    if users[name]["position"] <= GOAL_POSITION:
        users[name]["position"] += SPACES[users[name]["position"]]["action"]

    move_to_next_player()

    if users[name]["position"] >= GOAL_POSITION:
        users[name]["position"] = GOAL_POSITION
        users[name]["finished"] = True
        effect_message = SPACES[GOAL_POSITION]["message"]
    else:
        effect_message = SPACES[users[name]["position"]]["message"]

    position = users[name]["position"]

    response = {
        "name": name,
        "dice": dice,
        "position": position,
        "effect": effect_message
    }

    return jsonify(response), 200

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=8888)

まとめ

とりあえず、遊べる状態まで持っていきました。
次回は、いよいよ本題の「AIキャラクター側への組み込み」をしていきます。
また、並行してゲーム内容やGUIの作成も進めていきます!

この記事が気に入ったらサポートをしてみませんか?