AI异步玩《黑手党》,是否更像人了?

问题引入

不知道大家有没有在B站上看过AI狼人杀的视频?视频中虽然各个AI通过推理进行狼人杀游戏,但是各个AI自身依然保持着“你问我答”的回合制局面。
然而就在上个月,有研究者就提出了“让AI决定自己什么时候能说话”:论文地址
在这里插入图片描述

原理实际上很简单:在每次AI生成之前,都预先进行一轮对话:让AI自己决定要不要加入到对话中。如果选择加入,再进入对话生成的进程中。如此这样不断地调用对应函数,使得AI能够更像人一样,异步地进行对话。同时让AI动态平衡,若AI发言频率低于群体均值,提示“更积极”。除此之外还模拟了AI打字的速度。
核心的代码以及实验结论可以在github中看到。
作者还在他的代码里写了另外3种策略:先生成后判断是否发送、固定频率进行生成、使用微调后的模型。

可视化复用

复用后的git链接
我想要复用这些代码;同时加入可视化要素(Gradio中游玩)。
由于我没有多人游玩的条件,所以我决定在原作者的基础上进行如下改变:
1、从1个AI多个真人变为1个真人多个AI
2、让各个不同的AI之间使用不同的发言频率。

主进程

原作者的游戏大致思路是:不同的玩家进行的操作都会写入特定的txt文件中。每个阶段主持人bot只需要读这些txt文件就能进行不同的游戏阶段推进。这部分我不需要做过多的更改。

import json
import os
from GameConsts import *
from dataclasses import dataclass, asdict, field
from pathlib import Path

game_dir = Path(os.curdir+"/Game/")

@dataclass
class PlayerConfig:
    name: str  # player's code name from the game's pool (in constants file)
    is_mafia: bool = False
    is_llm: bool = False
    real_name: str = ""
    llm_config: dict = field(default_factory=dict)

def init_game():
    for filename in os.listdir(os.curdir+"/Game/"):
        file_path = os.path.join(os.curdir+"/Game/", filename)
        os.remove(file_path)

    with open("config.json", "r",encoding="utf-8") as original_file:
        config = json.load(original_file)
    config["config_original_path_when_game_created"] = str(game_dir/"config.json")
    with open(game_dir / GAME_CONFIG_FILE, "w",encoding="utf-8") as output_file:
        json.dump(config, output_file, indent=4, ensure_ascii=False)
    players = [PlayerConfig(**player_config) for player_config in config[PLAYERS_KEY_IN_CONFIG]]
    all_names_str = "\n".join([player.name for player in players])
    (game_dir / PLAYER_NAMES_FILE).write_text(all_names_str,encoding="utf-8")
    (game_dir / REMAINING_PLAYERS_FILE).write_text(all_names_str,encoding="utf-8")
    all_mafia_names_str = [player.name for player in players if player.is_mafia]
    (game_dir / MAFIA_NAMES_FILE).write_text("\n".join(all_mafia_names_str),encoding="utf-8")
    real_name_to_codename_str = [f"{player.real_name}{REAL_NAME_CODENAME_DELIMITER}{player.name}"
                                 for player in players if not player.is_llm]
    (game_dir / REAL_NAMES_FILE).write_text("\n".join(real_name_to_codename_str),encoding="utf-8")
    (game_dir / PHASE_STATUS_FILE).write_text(DAYTIME,encoding="utf-8")
    (game_dir / PUBLIC_MANAGER_CHAT_FILE).touch()
    (game_dir / PUBLIC_DAYTIME_CHAT_FILE).touch()
    (game_dir / PUBLIC_NIGHTTIME_CHAT_FILE).touch()
    (game_dir / WHO_WINS_FILE).touch()
    (game_dir / GAME_START_TIME_FILE).touch()
    (game_dir / NOTES_FILE).touch()
    for player in players:
        (game_dir / PERSONAL_CHAT_FILE_FORMAT.format(player.name)).touch()
        (game_dir / PERSONAL_VOTE_FILE_FORMAT.format(player.name)).touch()
        (game_dir / PERSONAL_STATUS_FILE_FORMAT.format(player.name)).touch()
        if player.is_llm:
            (game_dir / LLM_LOG_FILE_FORMAT.format(player.name)).touch()
        else:
            (game_dir / PERSONAL_SURVEY_FILE_FORMAT.format(player.name)).touch()
    # since for some reason the `mode` arg in mkdir doesn't work properly:
    # os.system(f"chmod -R 777 {game_dir}")
    print(f"Successfully created a new game dir in: {game_dir.absolute()}")


class Player:

    def __init__(self, name, is_mafia, **kwargs):
        self.name = name
        self.is_mafia = is_mafia
        self.personal_chat_file = game_dir / PERSONAL_CHAT_FILE_FORMAT.format(self.name)
        self.personal_chat_file_lines_read = 0
        self.personal_vote_file = game_dir / PERSONAL_VOTE_FILE_FORMAT.format(self.name)
        self.personal_vote_file_lines_read = 0
        # status is whether the player has joined and then whether was voted out
        self.personal_status_file = game_dir / PERSONAL_STATUS_FILE_FORMAT.format(self.name)

    def get_new_messages(self):
        with open(self.personal_chat_file, "r",encoding="utf-8") as f:
            # the readlines method includes the "\n"
            lines = f.readlines()[self.personal_chat_file_lines_read:]
        self.personal_chat_file_lines_read += len(lines)
        return lines

    def get_voted_player(self):
        all_votes = self.personal_vote_file.read_text(encoding="utf-8").splitlines()
        new_votes = all_votes[self.personal_vote_file_lines_read:]
        if new_votes:
            self.personal_vote_file_lines_read += len(new_votes)  # should be 1 if works correctly
            return new_votes[-1].strip()
        else:
            return None

    def eliminate(self):
        self.personal_status_file.write_text(VOTED_OUT,encoding="utf-8")

def get_config():
    with open(game_dir / GAME_CONFIG_FILE, "r",encoding="utf-8") as f:
        config = json.load(f)
    return config


def get_players(config):
    return [Player(**player_config) for player_config in config[PLAYERS_KEY_IN_CONFIG]]


def is_win_by_bystanders(mafia_players):
    if len(mafia_players) == 0:
        (game_dir / WHO_WINS_FILE).write_text("旁观者获胜",encoding="utf-8")
        return True
    return False


def is_win_by_mafia(mafia_players, bystanders):
    if len(mafia_players) >= len(bystanders):
        (game_dir / WHO_WINS_FILE).write_text("黑手党获胜",encoding="utf-8")
        return True
    return False


def is_game_over(players):
    mafia_players = [player for player in players if player.is_mafia]
    bystanders = [player for player in players if not player.is_mafia]
    return is_win_by_bystanders(mafia_players) or is_win_by_mafia(mafia_players, bystanders)


def run_chat_round_between_players(players, chat_room):
    for player in players:
        lines = player.get_new_messages()
        with open(chat_room, "a",encoding="utf-8") as f:
            f.writelines(lines)  # lines already include "\n"


def notify_players_about_voting_time(phase_name, public_chat_file):
    phase_end_message = DAYTIME_VOTING_TIME_MESSAGE if phase_name == DAYTIME else NIGHTTIME_VOTING_TIME_MESSAGE
    with open(public_chat_file, "a",encoding="utf-8") as f:  # only to the current phase's active players chat room
        f.write(format_message(GAME_MANAGER_NAME, phase_end_message))
    voting_phase_name = DAYTIME_VOTING_TIME if phase_name == DAYTIME else NIGHTTIME_VOTING_TIME
    (game_dir / PHASE_STATUS_FILE).write_text(voting_phase_name,encoding="utf-8")



def get_voted_out_name(optional_votes_players, public_chat_file, voting_players):
    votes = {player.name: 0 for player in optional_votes_players}
    while voting_players:
        voted_players = []
        for player in voting_players:
            voted_for = player.get_voted_player()
            if not voted_for:
                continue
            voted_players.append(player)
            if voted_for in votes:
                with open(public_chat_file, "a",encoding="utf-8") as f:
                    voting_message = VOTING_MESSAGE_FORMAT.format(player.name, voted_for)
                    f.write(format_message(GAME_MANAGER_NAME, voting_message))
                votes[voted_for] += 1
        for player in voted_players:
            voting_players.remove(player)
    # if there were invalid votes or if there was a tie, decision will be made "randomly"
    voted_out_name = max(votes, key=votes.get)
    return voted_out_name


def voting_sub_phase(phase_name, voting_players, optional_votes_players, public_chat_file, players):
    notify_players_about_voting_time(phase_name, public_chat_file)
    voted_out_name = get_voted_out_name(optional_votes_players, public_chat_file, voting_players[:])
    # update info file of remaining players
    remaining_players = (game_dir / REMAINING_PLAYERS_FILE).read_text(encoding="utf-8").splitlines()
    remaining_players.remove(voted_out_name)
    (game_dir / REMAINING_PLAYERS_FILE).write_text("\n".join(remaining_players),encoding="utf-8")
    # update player object status
    voted_out_player = {player.name: player for player in optional_votes_players}[voted_out_name]
    voted_out_player.eliminate()
    players.remove(voted_out_player)
    announce_voted_out_player(voted_out_player)


def game_manager_announcement(message):
    with open(game_dir / PUBLIC_MANAGER_CHAT_FILE, "a",encoding="utf-8") as f:
        f.write(format_message(GAME_MANAGER_NAME, message))

def get_role_string(is_mafia):
    return MAFIA_ROLE if is_mafia else BYSTANDER_ROLE
VOTED_OUT_MESSAGE_FORMAT = "{}被投票出局。他的角色是{}"

def announce_voted_out_player(voted_out_player):
    role = get_role_string(voted_out_player.is_mafia)
    voted_out_message = VOTED_OUT_MESSAGE_FORMAT.format(voted_out_player.name, role)
    game_manager_announcement(voted_out_message)

CUTTING_TO_VOTE_MESSAGE = "只剩1位黑手党了,无需讨论直接投票!"

def run_phase(players, voting_players, optional_votes_players, public_chat_file,
              time_limit_seconds, phase_name):
    if len(voting_players) > 1:
        start_time = time.time()
        while time.time() - start_time < time_limit_seconds:
            run_chat_round_between_players(voting_players, public_chat_file)
    else:
        game_manager_announcement(CUTTING_TO_VOTE_MESSAGE)
    print("Now voting starts...")
    voting_sub_phase(phase_name, voting_players, optional_votes_players, public_chat_file, players)



def minutes_to_seconds(num_minutes):
    return int(num_minutes * 60)

def run_nighttime(players, nighttime_minutes):
    (game_dir / PHASE_STATUS_FILE).write_text(NIGHTTIME,encoding="utf-8")
    mafia_players = [player for player in players if player.is_mafia]
    bystanders = [player for player in players if not player.is_mafia]
    print(colored(NIGHTTIME_START_MESSAGE_FORMAT.format(nighttime_minutes), NIGHTTIME_COLOR))
    game_manager_announcement(NIGHTTIME_START_MESSAGE_FORMAT.format(nighttime_minutes))
    run_phase(players, mafia_players, bystanders, game_dir / PUBLIC_NIGHTTIME_CHAT_FILE,
              minutes_to_seconds(nighttime_minutes), NIGHTTIME)


def run_daytime(players, daytime_minutes):
    (game_dir / PHASE_STATUS_FILE).write_text(DAYTIME,encoding="utf-8")
    print(colored(DAYTIME_START_MESSAGE_FORMAT.format(daytime_minutes), DAYTIME_COLOR))
    game_manager_announcement(DAYTIME_START_MESSAGE_FORMAT.format(daytime_minutes))
    run_phase(players, players, players, game_dir / PUBLIC_DAYTIME_CHAT_FILE,
              minutes_to_seconds(daytime_minutes), DAYTIME)


def wait_for_players(players):
    havent_joined_yet = [player for player in players]
    print("Waiting for all players to connect and start running their programs to join:")
    print(",  ".join([player.name for player in havent_joined_yet]))
    while havent_joined_yet:
        joined = []
        for player in havent_joined_yet:
            if bool(player.personal_status_file.read_text(encoding="utf-8")):  # file isn't empty once joined
                joined.append(player)
                print(f"{player.name} has joined!")
        for player in joined:
            havent_joined_yet.remove(player)
    (game_dir / GAME_START_TIME_FILE).write_text(get_current_timestamp(),encoding="utf-8")
    print("Game is now running! Its content is displayed to players.")


def get_all_player_out_of_voting_time():
    current_phase = (game_dir / PHASE_STATUS_FILE).read_text(encoding="utf-8")
    (game_dir / PHASE_STATUS_FILE).write_text(current_phase.replace(VOTING_TIME, ""),encoding="utf-8")


def end_game():
    get_all_player_out_of_voting_time()
    print("Game has finished.")


def main():
    init_game()
    config = get_config()
    players = get_players(config)
    wait_for_players(players)
    while not is_game_over(players):
        run_daytime(players, config[DAYTIME_MINUTES_KEY])
        if is_game_over(players):
            break
        run_nighttime(players, config[NIGHTTIME_MINUTES_KEY])
    end_game()

if __name__ == '__main__':
    main()

AI Player

对于AI机器人而言,原作者设计了LLM类,给了它生成对话、写入txt文件以及投票等功能。根据不同的策略来继承这个母类。我只需要ScheduleThenGeneratePlayer方法就可以了。

class Logger:
    def __init__(self, name: str, game_dir: Path):
        LLM_LOG_FILE_FORMAT = "{}_log.txt"
        self.log_file = game_dir / LLM_LOG_FILE_FORMAT.format(name)

    def log(self, operation, content):
        with open(self.log_file, "a",encoding="utf-8") as f:
            f.write(NEW_LOG_FORMAT.format(time=time.strftime("%H:%M:%S"),
                                          operation=operation, content=content))
										  
class LLMWrapper:

    def __init__(self, logger, **llm_config):
        self.logger = logger

        self.ipt_token = 0
        self.output_token = 0

        self.model_name = llm_config[MODEL_NAME_KEY]
        self.enable_thinking = llm_config[ENABLE_THINKING]
        self.generation_parameters = {key: value for key, value in llm_config.items() if key not in [ENABLE_THINKING,FREQUENCY,STYLE,MODEL_NAME_KEY,PASS_TURN_TOKEN_KEY,USE_TURN_TOKEN_KEY]}
        if 'stream' in self.generation_parameters and self.generation_parameters["stream"]:
            self.generation_parameters["stream_options"] = {"include_usage": True}
        self.client = OpenAI(
            api_key=os.getenv("DASHSCOPE_API_KEY"),
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1/",
        )
        self.generate(INITIAL_GENERATION_PROMPT, system_info=GENERAL_SYSTEM_INFO)
        

    def generate(self, input_text, system_info="", generation_parameters=None):
        #if generation_parameters is None:
        generation_parameters = self.generation_parameters
        messages = [{"role": "system", "content": system_info}] if system_info else []
        messages += [{"role": "user", "content": input_text}]
        self.logger.log("messages in generate with self.use_together", messages)
        final_output = self.generate_with_together_safely(messages, generation_parameters)  # max_new_tokens -> max_tokens
        self.logger.log("final_output in generate with self.use_together", final_output)
    
        return final_output.replace("\n", "   ").strip()

    def generate_with_together_safely(self, messages, generation_parameters):
        output = None
        while not output:
            try:
                response = self.client.chat.completions.create(
                    model=self.model_name,
                    messages=messages,
                    extra_body={"enable_thinking": self.enable_thinking},
                    **generation_parameters
                )
                if 'stream' in generation_parameters and generation_parameters["stream"]:
                    output = ""
                    for chunk in response:
                        if not chunk.choices:
                            continue
                        if chunk.choices[0].delta.content:
                            output += chunk.choices[0].delta.content
                    self.ipt_token += chunk.usage.prompt_tokens
                    self.output_token += chunk.usage.completion_tokens
                else:
                    output = response.choices[0].message.content
                    self.ipt_token += response.usage.prompt_tokens
                    self.output_token += response.usage.completion_tokens
            except Exception as e:
                print(f"TogetherException\n{e}")
                print(generation_parameters)
                self.logger.log("error generating with TogetherAI", str(e))
                time.sleep(SLEEPING_TIME_FOR_API_GENERATION_ERROR)
        return output

class LLMPlayer(ABC):

    TYPE_NAME = None

    def __init__(self, name, is_mafia, llm_config, game_dir, **kwargs):
        self.name = name
        self.is_mafia = is_mafia
        self.role = MAFIA_ROLE if is_mafia else BYSTANDER_ROLE
        self.game_dir = game_dir
        self.logger = Logger(name, game_dir)
        self.pass_turn_token = llm_config[PASS_TURN_TOKEN_KEY]
        self.use_turn_token = llm_config[USE_TURN_TOKEN_KEY]
        self.llm = LLMWrapper(self.logger, **llm_config)
        self.frequency = float(llm_config[FREQUENCY])
        self.style = llm_config[STYLE]

    def get_system_info_message(self, attention_to_not_repeat=False, only_special_tokens=False):
        system_info = f"你的名字是{self.name}. {GENERAL_SYSTEM_INFO}\n" \
                      f"你被分配到的角色是: {self.role}.\n"
        chat_room_open_time = (self.game_dir / GAME_START_TIME_FILE).read_text(encoding="utf-8").strip()
        if chat_room_open_time:  # if the game has started, the file isn't empty
            system_info += f"游戏房间在{chat_room_open_time}开启.\n"
        if attention_to_not_repeat:
            # system_info += "Note: Do not repeat any messages already present in the message history below!\n"
            system_info += RULES
            previous_messages = (self.game_dir / PERSONAL_CHAT_FILE_FORMAT.format(self.name)
                                 ).read_text(encoding="utf-8").splitlines()
            if previous_messages:
                system_info += "以下是你以前发过的信息,请勿重复:"
                for message in previous_messages:
                    matcher = re.match(MESSAGE_PARSING_PATTERN, message)
                    if not matcher:
                        continue
                    message_content = matcher.group(5)  # depends on MESSAGE_PARSING_PATTERN
                    system_info += f"* \"{message_content}\"\n"
        if only_special_tokens:
            system_info += f"你只能在以下的2个选项(包括左右的<>)中进行回复\n" \
                           f"{self.pass_turn_token} - 代表你所扮演的角色" \
                           f"应当静观其变,等待并且不说任何话;\n" \
                           f"{self.use_turn_token} - 代表你所扮演的角色" \
                           f"应当现在就进行发言。\n\n" \
                           f"你绝不能输出任何其他文本、解释或者选项变化" \
                           f"只允许使用以下选项:" \
                           f"{self.pass_turn_token}{self.use_turn_token}.\n"
        return system_info

    @abstractmethod
    def should_generate_message(self, context):
        raise NotImplementedError()

    @abstractmethod
    def generate_message(self, message_history):
        raise NotImplementedError()

    def interpret_scheduling_decision(self, decision):
        if not decision:
            generate = False
        elif self.pass_turn_token in decision:
            generate = False
        elif self.use_turn_token in decision:
            generate = True
        # for more robustness:
        # elif any([option in decision for option in PASS_TURN_TOKEN_OPTIONS]):
        #     generate = False
        else:
            generate = True
        if generate:
            self.logger.log(SCHEDULING_DECISION_LOG, MODEL_CHOSE_TO_USE_TURN_LOG)
        else:
            self.logger.log(SCHEDULING_DECISION_LOG, MODEL_CHOSE_TO_PASS_TURN_LOG)
        return generate

    def get_vote(self, message_history, candidate_vote_names):
        task = f"在剩下的玩家中,你想要淘汰哪位?" \
               f"请根据历史记录中的对话内容来回答。" \
               f"请只回复一个名字,除了这个名字以外什么都不要回复"
        task += ", ".join(candidate_vote_names)
        prompt = turn_task_into_prompt(task, message_history)
        system_info = self.get_system_info_message()
        self.logger.log("prompt for get_vote", prompt)
        self.logger.log("system_info for get_vote", system_info)
        vote = self.llm.generate(prompt, system_info)
        self.logger.log("generated vote in get_vote", vote)
        return vote

def no_one_has_talked_yet_in_current_phase(message_history):
    if not message_history:
        return True
    matcher = re.match(MESSAGE_PARSING_PATTERN, message_history[-1])
    if not matcher:
        return True
    name = matcher.group(4)  # depends on MESSAGE_PARSING_PATTERN
    return name == GAME_MANAGER_NAME


class ScheduleThenGeneratePlayer(LLMPlayer):

    TYPE_NAME = SCHEDULE_THEN_GENERATE_TYPE

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.scheduler = self.llm  # using the same one for generation...

    def should_generate_message(self, message_history):
        if no_one_has_talked_yet_in_current_phase(message_history):
            return False
        prompt = self.create_scheduling_prompt(message_history)
        self.logger.log("prompt in should_generate_message", prompt)
        decision = self.scheduler.generate(
            prompt, self.get_system_info_message(only_special_tokens=True),
            SCHEDULING_GENERATION_PARAMETERS)
        self.logger.log("decision in should_generate_message", decision)
        return self.interpret_scheduling_decision(decision)

    def generate_message(self, message_history):
        if self.should_generate_message(message_history):
            prompt = self.create_generation_prompt(message_history)
            self.logger.log("prompt in generate_message", prompt)
            message = self.llm.generate(
                prompt, self.get_system_info_message(attention_to_not_repeat=True))
            message = make_more_human_like(message)
            return message
        else:
            return ""

    def talkative_scheduling_prompt_modifier(self, message_history):
        if not message_history or is_nighttime(self.game_dir):
            return TALKATIVE_PROMPT
        all_players = (self.game_dir / REMAINING_PLAYERS_FILE).read_text(encoding="utf-8").splitlines()
        players_counts = {player: 0 for player in all_players}
        for message in message_history[::-1]:
            for player in players_counts:
                if f"] {player}: " in message:
                    players_counts[player] += 1
        all_player_messages = sum(players_counts.values())
        if not all_player_messages or players_counts[self.name] / all_player_messages < self.frequency:
            return TALKATIVE_PROMPT
        else:
            return QUIETER_PROMPT

    def create_scheduling_prompt(self, message_history):
        # removed these because of too many talks:
        # "If one of the last messages has mentioned you, then choose to send a message now."
        task = f"你现在是想要向其他玩家发送消息,还是静观其变,看看其他人会发送什么消息?" \
               f"只有当你觉得对当前讨论的贡献足够有意义时,才选择发送消息。" \
               f"{self.talkative_scheduling_prompt_modifier(message_history).strip()} " \
               f"根据现在的讨论,如果你想发送消息的话请只回复`{self.use_turn_token}`(包括左右的<>)," \
               f"如果想要静观其变的话请只回复`{self.pass_turn_token}`(包括左右的<>)。 "
        return turn_task_into_prompt(task, message_history)

    def create_generation_prompt(self, message_history):
        task = f"请在游戏聊天中加入一条的消息,只要给出语言就行了,不要给出动作、神态或其他描写,就好像是网上聊天发出的消息一样。" \
               f"字数不要超过100字,但是不要再回复中写字数的统计信息。"\
               f"请根据最后的消息以及现在的游戏状态,给出一个具体并且与现状相关的消息。" \
               f"**重要:不要重复自己说过的话!" \
               f"你的语言风格应当是:{self.style}"
        return turn_task_into_prompt(task, message_history)


OPERATOR_COLOR = "yellow"
LLM_PLAYER_LOADED_MESSAGE = "AI玩家载入成功,正在等待其他玩家……"
ALL_PLAYERS_JOINED_MESSAGE = "所有玩家已经就绪,游戏开始。"
LLM_VOTE_MESSAGE_FORMAT = "AI玩家投票: {}"
GAME_ENDED_MESSAGE = "游戏结束!"
GET_LLM_PLAYER_NAME_MESSAGE = "本轮游戏配置有多个AI玩家,你希望使用哪一个?"
ELIMINATED_MESSAGE = "AI玩家被投票出局了..."


# global variable
game_dir = Path()  # will be updated in get_llm_player


def get_llm_player(role):
    global game_dir
    game_dir = Path(os.curdir+"/Game/")
    with open(game_dir / GAME_CONFIG_FILE,encoding="utf-8") as f:
        config = json.load(f)
    llm_players_configs = [player for player in config[PLAYERS_KEY_IN_CONFIG] if player["is_llm"]]
    if not llm_players_configs:
        raise ValueError("本游戏没有配置AI玩家")
    elif len(llm_players_configs) == 1:
        player_config = llm_players_configs[0]
    else:
        # player_name = get_player_name_from_user([player["name"] for player in llm_players_configs],
        #                                         GET_LLM_PLAYER_NAME_MESSAGE, OPERATOR_COLOR)
        player_name = role
        player_config = [player for player in llm_players_configs
                         if player["name"] == player_name][0]
    player_config[GAME_DIR_KEY] = game_dir
    llm_player = ScheduleThenGeneratePlayer(**player_config)
    (game_dir / PERSONAL_STATUS_FILE_FORMAT.format(llm_player.name)).write_text(JOINED,encoding="utf-8")
    return llm_player


def read_messages_from_file(message_history, file_name, num_read_lines):
    with open(game_dir / file_name, "r",encoding="utf-8") as f:
        lines = f.readlines()[num_read_lines:]
    message_history.extend(lines)
    return len(lines)


def wait_writing_time(player, message):
    time.sleep(MAX_TIME_TO_WAIT)


def eliminate(player):
    # currently doesn't use player, but maybe in the future we can use player.logger for example
    print(colored(ELIMINATED_MESSAGE, OPERATOR_COLOR))


def get_vote_from_llm(player, message_history):
    candidate_vote_names = (game_dir / REMAINING_PLAYERS_FILE).read_text(encoding="utf-8").splitlines()
    candidate_vote_names.remove(player.name)
    voting_message = player.get_vote(message_history, candidate_vote_names)
    for name in candidate_vote_names:
        if name in voting_message:  # update game manger
            update_vote(name, player)
            return
    # if didn't return: no name was in voting_message
    player.logger.log(MODEL_VOTED_INVALIDLY_LOG, voting_message)
    print(colored(MODEL_VOTED_INVALIDLY_LOG + ": " + voting_message, OPERATOR_COLOR))
    vote = random.choice(candidate_vote_names)
    player.logger.log(MODEL_RANDOMLY_VOTED_LOG, vote)
    update_vote(vote, player)


def update_vote(voted_name, player):
    time.sleep(VOTING_WAITING_TIME)
    with open(game_dir / PERSONAL_VOTE_FILE_FORMAT.format(player.name), "a",encoding="utf-8") as f:
        f.write(voted_name + "\n")
    print(colored(LLM_VOTE_MESSAGE_FORMAT.format(voted_name), OPERATOR_COLOR))


def add_message_to_game(player, message_history):
    is_nighttime_at_start = is_nighttime(game_dir)
    if not player.is_mafia and is_nighttime_at_start:
        return  # only mafia can communicate during nighttime
    message = player.generate_message(message_history).strip()
    if is_time_to_vote(game_dir):
        return  # sometimes the messages is generated when it's already too late, so drop it
    if message:
        # artificially making the model taking time to write the message
        wait_writing_time(player, message)
        if is_nighttime(game_dir) != is_nighttime_at_start:
            return  # waited for too long
        with open(game_dir / PERSONAL_CHAT_FILE_FORMAT.format(player.name), "a",encoding="utf-8") as f:
            f.write(format_message(player.name, message))
        print(colored(MODEL_CHOSE_TO_USE_TURN_LOG, OPERATOR_COLOR))
    else:
        print(colored(MODEL_CHOSE_TO_PASS_TURN_LOG, OPERATOR_COLOR))


def end_game():
    print(colored(GAME_ENDED_MESSAGE, OPERATOR_COLOR))


def main(role):
    player = get_llm_player(role)
    print(colored(LLM_PLAYER_LOADED_MESSAGE, OPERATOR_COLOR))
    while not all_players_joined(game_dir):
        continue
    print(colored(ALL_PLAYERS_JOINED_MESSAGE, OPERATOR_COLOR))
    message_history = []
    num_read_lines_manager = num_read_lines_daytime = num_read_lines_nighttime = 0
    while not is_game_over(game_dir):
        num_read_lines_manager += read_messages_from_file(
            message_history, PUBLIC_MANAGER_CHAT_FILE, num_read_lines_manager)
        # only current phase file will have new messages, so no need to run expensive is_nighttime()
        num_read_lines_daytime += read_messages_from_file(
            message_history, PUBLIC_DAYTIME_CHAT_FILE, num_read_lines_daytime)
        if player.is_mafia:  # only mafia can see what happens during nighttime
            num_read_lines_nighttime += read_messages_from_file(
                message_history, PUBLIC_NIGHTTIME_CHAT_FILE, num_read_lines_nighttime)
        if is_voted_out(player.name, game_dir):
            eliminate(player)
            break
        if is_time_to_vote(game_dir) and (player.is_mafia or not is_nighttime(game_dir)):
            get_vote_from_llm(player, message_history)
            while is_time_to_vote(game_dir):
                continue  # wait for voting time to end when all players have voted
        add_message_to_game(player, message_history)
    end_game()
    with open(role+"Used Token.txt","w",encoding="utf-8") as f:
        f.write(f"输入token:{player.llm.ipt_token} 输出token:{player.llm.output_token}")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='选择角色')
    parser.add_argument('--role', help='选择的角色')
    args = parser.parse_args()
    try:
        main(args.role)
    except Exception as e:
        with open(f"{args.role}_error.txt","w") as f:
            f.write(e)

Gradio可视化

我没有使用原作者对于人类写的交互类,反而是模仿原本的LLMPlayer类,写了一个Human类出来。在Gradio中,使用一个隐形的按钮来按特定间隔扫描所有的txt文件,从而让人能够在Gradio界面中展示对应的界面。

from pathlib import Path
import time
import os
import json
from GameConsts import *
from pathlib import Path
from AIPlayer import Logger
import gradio as gr
										  
class Human:

    def __init__(self, **params):
        self.logger = Logger
        self.name = params["name"]
        self.is_mafia = params["is_mafia"]

    def speak(self, s):
        if generation_parameters is None:
            generation_parameters = self.generation_parameters
        self.logger.log("final_output", s)
    
        return s

    def get_vote(self,vote):
        self.logger.log("generated vote in get_vote", vote)
        return vote

OPERATOR_COLOR = "yellow"
HUMAN_PLAYER_LOADED_MESSAGE = "人类玩家初始化成功,正在等待其他玩家……"
ALL_PLAYERS_JOINED_MESSAGE = "所有玩家已经就绪,游戏开始。"
HUMAN_VOTE_MESSAGE_FORMAT = "人类玩家投票: {}"
GAME_ENDED_MESSAGE = "游戏结束!"
GET_HUMAN_PLAYER_NAME_MESSAGE = "本轮游戏配置有多个人类玩家,你希望使用哪一个?"
ELIMINATED_MESSAGE = "人类玩家被投票出局了..."


# global variable
game_dir = Path()  # will be updated in get_llm_player


def get_human_player():
    global game_dir
    game_dir = Path(os.curdir+"/Game/")
    with open(game_dir / GAME_CONFIG_FILE,encoding="utf-8") as f:
        config = json.load(f)
    human_players_configs = [player for player in config[PLAYERS_KEY_IN_CONFIG] if not player["is_llm"]]
    if not human_players_configs:
        raise ValueError("本游戏没有配置人类玩家")
    elif len(human_players_configs) == 1:
        player_config = human_players_configs[0]
    else:
        player_name = get_player_name_from_user([player["name"] for player in human_players_configs],
                                                GET_HUMAN_PLAYER_NAME_MESSAGE, OPERATOR_COLOR)
        player_config = [player for player in human_players_configs
                         if player["name"] == player_name][0]
    player_config[GAME_DIR_KEY] = game_dir
    human_player = Human(**player_config)
    (game_dir / PERSONAL_STATUS_FILE_FORMAT.format(human_player.name)).write_text(JOINED,encoding="utf-8")
    return human_player


def read_messages_from_file(message_history, file_name, num_read_lines):
    with open(game_dir / file_name, "r",encoding="utf-8") as f:
        lines = f.readlines()[num_read_lines:]
    message_history.extend(lines)
    return len(lines)


def wait_writing_time(player, message):
    time.sleep(MAX_TIME_TO_WAIT)


def eliminate(player):
    # currently doesn't use player, but maybe in the future we can use player.logger for example
    print(colored(ELIMINATED_MESSAGE, OPERATOR_COLOR))


def update_vote(voted_name, player:Human):
    time.sleep(VOTING_WAITING_TIME)
    with open(game_dir / PERSONAL_VOTE_FILE_FORMAT.format(player.name), "a",encoding="utf-8") as f:
        f.write(voted_name + "\n")
    print(colored(HUMAN_VOTE_MESSAGE_FORMAT.format(voted_name), OPERATOR_COLOR))


def end_game():
    print(colored(GAME_ENDED_MESSAGE, OPERATOR_COLOR))

class Interface_Gradio:
    def __init__(self) -> None: #后续改为 左3AI右3AI中间2的布局
        with open(game_dir / GAME_CONFIG_FILE,encoding="utf-8") as f:
            self.config = json.load(f)
        self.already_vote = False
        self.last_vote_time = None
        self.other_player_dict = {}
        self.vote_other_player_btn = {}
        self.message_history = []
        self.num_read_lines_manager = self.num_read_lines_daytime = self.num_read_lines_nighttime = 0
        self.player = None
        self.is_alive = True
        with open("autoclick.js") as f:
            js_code = f.read()
        with open("GradioCss.css") as f:
            css = f.read()
        with gr.Blocks(js = js_code,css=css) as demo:
            with gr.Row():
                for player in self.config[PLAYERS_KEY_IN_CONFIG]:
                    with gr.Column(scale=1,min_width=160):
                        self.other_player_dict[player["name"]] = gr.Chatbot(label=player["name"],type="messages",elem_classes="character")
                        self.vote_other_player_btn[player["name"]] = gr.Button(value=player["name"],visible=False)
                for btn in self.vote_other_player_btn.values():
                    btn.click(fn=self.vote,inputs=btn,outputs=[i for i in self.vote_other_player_btn.values()])
            with gr.Row():
                self.other_player_dict["主持人"] = gr.Chatbot(label="主持人",type="messages",elem_classes="character")
            
            with gr.Row():
                self.input_box = gr.Textbox(label="信息",scale=4,interactive=False)
                self.send_box = gr.Button("等待AI加入",scale=1,interactive=False)

                self.input_box.submit(fn=self.add_message_to_game,inputs=self.input_box,outputs=self.input_box)
                self.send_box.click(fn=self.add_message_to_game,inputs=self.input_box,outputs=self.input_box)

            with gr.Row():
                self.hidden_btn2 = gr.HTML("<div onclick=autorefresh() id=\"hidden_btn\">加入游戏<div/>",elem_id="to_be_hidden")
                self.hidden_btn = gr.Button(visible=False,elem_id="sync")

                self.hidden_btn.click(fn=self.scan_to_read,inputs=[],outputs=[self.input_box,self.send_box]+[i for i in self.vote_other_player_btn.values()]+[i for i in self.other_player_dict.values()],show_progress=False)

        demo.launch()

    def parse_message(self):
        res_dict = {i:[] for i in self.other_player_dict.keys()}
        for msg in self.message_history:
            msg = msg[11:] #去掉时间
            msg_list = msg.split(": ")
            role = msg_list[0]
            content = "".join([i for i in msg_list[1:]])
            if role not in res_dict.keys():
                raise NameError()
            res_dict[role].extend([{"role":"assistant","content":content},{"role":"user","content":""}])
        return res_dict

    def vote(self,name):
        with open(game_dir / PERSONAL_VOTE_FILE_FORMAT.format(self.player.name), "a",encoding="utf-8") as f:
            f.write(name + "\n")
        self.already_vote = True
        if NIGHTTIME_VOTING_TIME in (game_dir / PHASE_STATUS_FILE).read_text(encoding="utf-8"):
            self.last_vote_time = "Night"
        else:
            self.last_vote_time = "Day"
        return [gr.update(visible=False)]*len(self.vote_other_player_btn.values())

    def scan_to_read(self):
        if self.player is None:
            self.player = get_human_player()
        if not all_players_joined(game_dir):
            send_available = [gr.update(),gr.update()]
            vote_btns = [gr.update()]*len(self.vote_other_player_btn.values())
            join_status = []
            for i in self.other_player_dict.keys():
                if i == "主持人":
                    join_status.append(gr.update())
                else:
                    status = bool((game_dir / PERSONAL_STATUS_FILE_FORMAT.format(i)).read_text(encoding="utf-8"))
                    join_status.append(gr.update(value=[{"role":"assistant","content":"已加入" if status else "等待加入..."}]))
            return send_available+vote_btns+join_status
        
        


        send_available = [gr.update(interactive=True),gr.update(interactive=True,value="发送")]
        vote_btns = [gr.update(visible=False)]*len(self.vote_other_player_btn.values())
        if not self.is_alive:
            send_available = [gr.update(interactive=False),gr.update(interactive=False,value="已出局")]
        
        self.num_read_lines_daytime += read_messages_from_file(
            self.message_history, PUBLIC_DAYTIME_CHAT_FILE, self.num_read_lines_daytime)
        if self.player.is_mafia:  # only mafia can see what happens during nighttime
            self.num_read_lines_nighttime += read_messages_from_file(
                self.message_history, PUBLIC_NIGHTTIME_CHAT_FILE, self.num_read_lines_nighttime)
        self.num_read_lines_manager+= read_messages_from_file(
            self.message_history, PUBLIC_MANAGER_CHAT_FILE, self.num_read_lines_manager)
        if is_voted_out(self.player.name, game_dir):
            eliminate(self.player)
            self.is_alive = False

        if not(is_time_to_vote(game_dir)): #只剩一个Mafia时会略过
            self.already_vote = False
        
        current_time = "Night" if NIGHTTIME_VOTING_TIME in (game_dir / PHASE_STATUS_FILE).read_text(encoding="utf-8") else "Day"
        if is_time_to_vote(game_dir) and (self.player.is_mafia or not is_nighttime(game_dir)) and self.is_alive and ((not self.already_vote) or current_time!=self.last_vote_time):
            send_available = [gr.update(interactive=False),gr.update(interactive=False,value="请投票")]
            candidate_vote_names = (game_dir / REMAINING_PLAYERS_FILE).read_text(encoding="utf-8").splitlines()
            candidate_vote_names.remove(self.player.name)
            for idx,btn in enumerate(list(self.vote_other_player_btn.values())):
                if btn.value in candidate_vote_names:
                    vote_btns[idx] = gr.update(visible=True)
        if is_game_over(game_dir):
            self.is_alive = False
        
        msg_role = self.parse_message()
        msg_class_dict = {i:"character" for i in self.other_player_dict.keys()}
        survive_names = (game_dir / REMAINING_PLAYERS_FILE).read_text(encoding="utf-8").splitlines()
        # 如果是夜间,平民聊天框的底色变暗,人类平民不再可以发送信息
        if is_nighttime(game_dir):
            for player in self.config[PLAYERS_KEY_IN_CONFIG]:
                if not player["is_mafia"]:
                    msg_class_dict[player["name"]] = "character_inactive"

                    if player["name"] == self.player.name:
                        send_available = [gr.update(interactive=False),gr.update(interactive=False,value="夜间平民无法说话")]
        # 白天恢复
        if (not is_nighttime(game_dir)) and (not is_time_to_vote(game_dir)) and self.is_alive:
            send_available = [gr.update(interactive=True),gr.update(interactive=True,value="发送")]
        # 死亡角色的背景框变成红色
        for player in self.config[PLAYERS_KEY_IN_CONFIG]:
            if player["name"] not in survive_names:
                msg_class_dict[player["name"]] = "character_dead"
        character_msg = []
        for k,v in self.other_player_dict.items():
            character_msg.append(gr.update(value = msg_role[k],elem_classes=msg_class_dict[k]))
        
        if is_game_over(game_dir):
            end_info = "游戏结束,"+(game_dir / WHO_WINS_FILE).read_text(encoding="utf-8")
            send_available = [gr.update(interactive=False),gr.update(interactive=False,value=end_info)]
            vote_btns = [gr.update(visible=False)]*len(self.vote_other_player_btn.values())
        
        return send_available+vote_btns+character_msg

    def add_message_to_game(self,message):
        with open(game_dir / PERSONAL_CHAT_FILE_FORMAT.format(self.player.name), "a",encoding="utf-8") as f:
            f.write(format_message(self.player.name, message))
        return gr.update(value=None)

def main():
    player = Interface_Gradio()
    # print(colored(ALL_PLAYERS_JOINED_MESSAGE, OPERATOR_COLOR))
    # message_history = []
    # num_read_lines_manager = num_read_lines_daytime = num_read_lines_nighttime = 0
    # while not is_game_over(game_dir):
    #     num_read_lines_manager += read_messages_from_file(
    #         message_history, PUBLIC_MANAGER_CHAT_FILE, num_read_lines_manager)
    #     # only current phase file will have new messages, so no need to run expensive is_nighttime()
    #     num_read_lines_daytime += read_messages_from_file(
    #         message_history, PUBLIC_DAYTIME_CHAT_FILE, num_read_lines_daytime)
    #     if player.is_mafia:  # only mafia can see what happens during nighttime
    #         num_read_lines_nighttime += read_messages_from_file(
    #             message_history, PUBLIC_NIGHTTIME_CHAT_FILE, num_read_lines_nighttime)
    #     if is_voted_out(player.name, game_dir):
    #         eliminate(player)
    #         break
    #     if is_time_to_vote(game_dir) and (player.is_mafia or not is_nighttime(game_dir)):
    #         # get_vote_from_human(player, message_history)
    #         while is_time_to_vote(game_dir):
    #             continue  # wait for voting time to end when all players have voted
    # end_game()

if __name__ == '__main__':
    main()
.pending{
    display: none;
}

.message-wrap > .container{
    display: none;
}

.user-row{
    display: none;
}

.hidden{
    display: none;
}

.progress-text {
    display: none !important; 
}

#to_be_hidden{
    border:solid;
    background-color: #e4e4e7;
    text-align: center;
    cursor: pointer;
}
#hidden{
    display: none;
}
.character_inactive{
    background-color: #e4e4e7;
}

.character_dead{
    background-color: #ff676754;
}
async () => {
    globalThis.autorefresh = () => {
        let button = document.querySelector("#sync");
        timer = setInterval(function() {
            button.click();
        }, 500);
        let button2 = document.querySelector("#to_be_hidden");
        button2.id = "hidden";
    }
}

效果与展望

和作者一样,我选择了8B的模型。但是不同之处在于,作者实验是将1个AI放入杀人游戏,我则是只有1个人类,剩下的都是AI。同时,作者让每个AI都使用和其他人一样的频率,但是我给每个AI都设置了不同的频率。
请添加图片描述
然而,或许是由于使用小参数模型,或者我的提示词写的不好,出现了某些人物“忘记自己是谁”以及“不断重复某句话”的情形。
在这里插入图片描述
在这里插入图片描述
仔细看看原作者的实验记录,实际上也有很多“重复发言”的问题
请添加图片描述

不过原作者也有一些看上去不错的实验记录。就我个人而言,对这一策略有2点担忧:一个是如果使用参数量大的模型,是否会因为推理时间过长导致模型发言的间隔过长?另外就是不断调用模型会不会导致耗费的金钱飙升?
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值