Example: Werewolf Game

img

Werewolf is a well-known social-deduction game, that involves an imaginary village where a few villagers are secretly werewolves, and the objective is to identify who they are before they eliminate all other players. It’s a good use case to demonstrate the interaction between multiple autonomous agents, each with its own objectives and the need for communication.

Let the adventure begin to unlock the potential of multi-agent applications with AgentScope!

Getting Started

Firstly, ensure that you have installed and configured AgentScope properly. Besides, we will involve the basic concepts of Model API, Agent, Msg, and Pipeline, as described in Tutorial-Concept.

Note: all the configurations and code for this tutorial can be found in examples/game_werewolf.

Step 1: Prepare Model API and Set Model Configs

As we discussed in the last tutorial, you need to prepare your model configurations into a JSON file for standard OpenAI chat API, FastChat, and vllm. More details and advanced usages such as configuring local models with POST API are presented in Tutorial-Model-API.

[
    {
        "config_name": "gpt-4-temperature-0.0",
        "model_type": "openai_chat",
        "model_name": "gpt-4",
        "api_key": "xxx",
        "organization": "xxx",
        "generate_args": {
            "temperature": 0.0
        }
    }
]

Step 2: Define the Roles of Each Agent

In the Werewolf game, agents assume a variety of roles, each endowed with distinctive abilities and objectives. Below, we will outline the agent classes corresponding to each role:

  • Villager: Ordinary townsfolk trying to survive.

  • Werewolf: Predators in disguise, aiming to outlast the villagers.

  • Seer: A villager with the power to see the true nature of one player each night.

  • Witch: A villager who can save or poison a player each night.

To implement your own agent, you need to inherit AgentBase and implement the reply function, which is executed when an agent instance is called via agent1(x).

from agentscope.agents import AgentBase
from agentscope.message import Msg


from typing import Optional, Union, Sequence

class MyAgent(AgentBase):
    def reply(self, x: Optional[Union[Msg, Sequence[Msg]]] = None) -> Msg:
        # Do something here
        ...
        return x

AgentScope provides several out-of-the-box Agents implements and organizes them as an Agent Pool. In this application, we use a built-in agent, DictDialogAgent. Here we give an example configuration of DictDialogAgent for a player assigned as the role of a werewolf:

{
    "class": "DictDialogAgent",
    "args": {
        "name": "Player1",
        "sys_prompt": "Act as a player in a werewolf game. You are Player1 and\nthere are totally 6 players, named Player1, Player2, Player3, Player4, Player5 and Player6.\n\nPLAYER ROLES:\nIn werewolf game, players are divided into two werewolves, two villagers, one seer, and one witch. Note only werewolves know who are their teammates.\nWerewolves: They know their teammates' identities and attempt to eliminate a villager each night while trying to remain undetected.\nVillagers: They do not know who the werewolves are and must work together during the day to deduce who the werewolves might be and vote to eliminate them.\nSeer: A villager with the ability to learn the true identity of one player each night. This role is crucial for the villagers to gain information.\nWitch: A character who has a one-time ability to save a player from being eliminated at night (sometimes this is a potion of life) and a one-time ability to eliminate a player at night (a potion of death).\n\nGAME RULE:\nThe game consists of two phases: night phase and day phase. The two phases are repeated until werewolf or villager wins the game.\n1. Night Phase: During the night, the werewolves discuss and vote for a player to eliminate. Special roles also perform their actions at this time (e.g., the Seer chooses a player to learn their role, the witch chooses a decide if save the player).\n2. Day Phase: During the day, all surviving players discuss who they suspect might be a werewolf. No one reveals their role unless it serves a strategic purpose. After the discussion, a vote is taken, and the player with the most votes is \"lynched\" or eliminated from the game.\n\nVICTORY CONDITION:\nFor werewolves, they win the game if the number of werewolves is equal to or greater than the number of remaining villagers.\nFor villagers, they win if they identify and eliminate all of the werewolves in the group.\n\nCONSTRAINTS:\n1. Your response should be in the first person.\n2. This is a conversational game. You should respond only based on the conversation history and your strategy.\n\nYou are playing werewolf in this game.\n",
        "model_config_name": "gpt-3.5-turbo",
        "use_memory": true
    }
}

In this configuration, Player1 is designated as a DictDialogAgent. The parameters include a system prompt (sys_prompt) that can guide the agent’s behavior, a model config name (model_config_name) that determines the name of the model configuration, and a flag (use_memory) indicating whether the agent should remember past interactions.

For other players, configurations can be customized based on their roles. Each role may have different prompts, models, or memory settings. You can refer to the JSON file located at examples/game_werewolf/configs/agent_configs.json within the AgentScope examples directory.

Step 3: Initialize AgentScope and the Agents

Now we have defined the roles in the application and we can initialize the AgentScope environment and all agents. This process is simplified by AgentScope via a few lines, based on the configuration files we’ve prepared (assuming there are 2 werewolves, 2 villagers, 1 witch, and 1 seer):

import agentscope

# read model and agent configs, and initialize agents automatically
survivors = agentscope.init(
    model_configs="./configs/model_configs.json",
    agent_configs="./configs/agent_configs.json",
    logger_level="DEBUG",
)

# Define the roles within the game. This list should match the order and number
# of agents specified in the 'agent_configs.json' file.
roles = ["werewolf", "werewolf", "villager", "villager", "seer", "witch"]

# Based on their roles, assign the initialized agents to variables.
# This helps us reference them easily in the game logic.
wolves, villagers, witch, seer = survivors[:2], survivors[2:-2], survivors[-1], survivors[-2]

Through this snippet of code, we’ve allocated roles to our agents and associated them with the configurations that dictate their behavior in the application.

Step 4: Set Up the Game Logic

In this step, you will set up the game logic and orchestrate the flow of the Werewolf game using AgentScope’s helper utilities.

Parser

In order to allow DictDialogAgent to output fields customized by the users, and to increase the success rate of parsing different fields by LLMs, we have added the parser module. Here is the configuration of a parser example:

to_wolves_vote = "Which player do you vote to kill?"

wolves_vote_parser = MarkdownJsonDictParser(
    content_hint={
        "thought": "what you thought",
        "vote": "player_name",
    },
    required_keys=["thought", "vote"],
    keys_to_memory="vote",
    keys_to_content="vote",
)

For more details about the parser module,please see here.

Leverage Pipeline and MsgHub

To simplify the construction of agent communication, AgentScope provides two helpful concepts: Pipeline and MsgHub.

  • Pipeline: It allows users to program communication among agents easily.

    from agentscope.pipelines import SequentialPipeline
    
    pipe = SequentialPipeline(agent1, agent2, agent3)
    x = pipe(x) # the message x will be passed and replied by agent 1,2,3 in order
    
  • MsgHub: You may have noticed that all the above examples are one-to-one communication. To achieve a group chat, we provide another communication helper utility msghub. With it, the messages from participants will be broadcast to all other participants automatically. In such cases, participating agents even don’t need input and output messages. All we need to do is to decide the order of speaking. Besides, msghub also supports dynamic control of participants.

    with msghub(participants=[agent1, agent2, agent3]) as hub:
        agent1()
        agent2()
    
        # Broadcast a message to all participants
        hub.broadcast(Msg("Host", "Welcome to join the group chat!"))
    
        # Add or delete participants dynamically
        hub.delete(agent1)
        hub.add(agent4)
    

Implement Werewolf Pipeline

The game logic is divided into two major phases: (1) night when werewolves act, and (2) daytime when all players discuss and vote. Each phase will be handled by a section of code using pipelines to manage multi-agent communications.

  • 1.1 Night Phase: Werewolves Discuss and Vote

During the night phase, werewolves must discuss among themselves to decide on a target. The msghub function creates a message hub for the werewolves to communicate in, where every message sent by an agent is observable by all other agents within the msghub.

# start the game
for i in range(1, MAX_GAME_ROUND + 1):
    # Night phase: werewolves discuss
    hint = HostMsg(content=Prompts.to_wolves.format(n2s(wolves)))
    with msghub(wolves, announcement=hint) as hub:
        set_parsers(wolves, Prompts.wolves_discuss_parser)
        for _ in range(MAX_WEREWOLF_DISCUSSION_ROUND):
            x = sequentialpipeline(wolves)
            if x.metadata.get("finish_discussion", False):
                break

After the discussion, werewolves proceed to vote for their target, and the majority’s choice is determined. The result of the vote is then broadcast to all werewolves.

Note: the detailed prompts and utility functions can be found in examples/game_werewolf.

        # werewolves vote
        set_parsers(wolves, Prompts.wolves_vote_parser)
        hint = HostMsg(content=Prompts.to_wolves_vote)
        votes = [extract_name_and_id(wolf(hint).content)[0] for wolf in wolves]
        # broadcast the result to werewolves
        dead_player = [majority_vote(votes)]
        hub.broadcast(
            HostMsg(content=Prompts.to_wolves_res.format(dead_player[0])),
        )
  • 1.2 Witch’s Turn

If the witch is still alive, she gets the opportunity to use her powers to either save the player chosen by the werewolves or use her poison.

    # Witch's turn
    healing_used_tonight = False
    if witch in survivors:
        if healing:
            # Witch decides whether to use the healing potion
            hint = HostMsg(
                content=Prompts.to_witch_resurrect.format_map(
                    {"witch_name": witch.name, "dead_name": dead_player[0]},
                ),
            )
            # Witch decides whether to use the poison
            set_parsers(witch, Prompts.witch_resurrect_parser)
            if witch(hint).metadata.get("resurrect", False):
                healing_used_tonight = True
                dead_player.pop()
                healing = False
  • 1.3 Seer’s Turn

The seer has a chance to reveal the true identity of a player. This information can be crucial for the villagers. The observe() function allows each agent to take note of a message without immediately replying to it.

    # Seer's turn
    if seer in survivors:
        # Seer chooses a player to reveal their identity
        hint = HostMsg(
            content=Prompts.to_seer.format(seer.name, n2s(survivors)),
        )
        set_parsers(seer, Prompts.seer_parser)
        x = seer(hint)

        player, idx = extract_name_and_id(x.content)
        role = "werewolf" if roles[idx] == "werewolf" else "villager"
        hint = HostMsg(content=Prompts.to_seer_result.format(player, role))
        seer.observe(hint)
  • 1.4 Update Alive Players

Based on the actions taken during the night, the list of surviving players needs to be updated.

    # Update the list of survivors and werewolves after the night's events
    survivors, wolves = update_alive_players(survivors, wolves, dead_player)
  • 2.1 Daytime Phase: Discussion and Voting

During the day, all players will discuss and then vote to eliminate a suspected werewolf.

    # Daytime discussion
    with msghub(survivors, announcement=hints) as hub:
        # Discuss
        set_parsers(survivors, Prompts.survivors_discuss_parser)
        x = sequentialpipeline(survivors)
        # Vote
        set_parsers(survivors, Prompts.survivors_vote_parser)
        hint = HostMsg(content=Prompts.to_all_vote.format(n2s(survivors)))
        votes = [extract_name_and_id(_(hint).content)[0] for _ in survivors]
        vote_res = majority_vote(votes)
        # Broadcast the voting result to all players
        result = HostMsg(content=Prompts.to_all_res.format(vote_res))
        hub.broadcast(result)
        # Update the list of survivors and werewolves after the vote
        survivors, wolves = update_alive_players(survivors, wolves, vote_res)
  • 2.2 Check for Winning Conditions

After each phase, the game checks if the werewolves or villagers have won.

        # Check if either side has won
        if check_winning(survivors, wolves, "Moderator"):
            break
  • 2.3 Continue to the Next Round

If neither werewolves nor villagers win, the game continues to the next round.

        # If the game hasn't ended, prepare for the next round
        hub.broadcast(HostMsg(content=Prompts.to_all_continue))

These code blocks outline the core game loop for Werewolf using AgentScope’s msghub and pipeline, which help to easily manage the operational logic of an application.

Step 5: Run the Application

With the game logic and agents set up, you’re ready to run the Werewolf game. By executing the pipeline, the game will proceed through the predefined phases, with agents interacting based on their roles and the strategies coded above:

cd examples/game_werewolf
python werewolf.py  # Assuming the pipeline is implemented in werewolf.py

It is recommended that you start the game in AgentScope Studio, where you will see the following output in the corresponding link:

s

[Return to the top]