DEV Community

Cover image for Creating Multi ReActAgents with NodeJS ( Talk itself into a League of legends Draft with NodeJS )
Allan Felipe Murara
Allan Felipe Murara

Posted on

Creating Multi ReActAgents with NodeJS ( Talk itself into a League of legends Draft with NodeJS )

Introduction

This project implements a League of Legends draft simulator using ReAct (Reason and Act) AI agents (10 simulated players).
If you don't know how to build a basic ReAct agent, checkout here

We simulate competitive draft process where AI-controlled players (10) make champion selections based on team compositions and strategies. The simulation uses IRC (Internet Relay Chat) for communication between agents and a moderator.

[1] Implementing the Agent

First, set up the project and install dependencies:

mkdir react-agents-lol-project
cd react-agents-lol-project
npm init -y
npm install dotenv @google/generative-ai irc
Enter fullscreen mode Exit fullscreen mode

1.1 Create a .env file at the project's root:

GOOGLE_AI_API_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

FREE ApiKey here

1.2 Creating the index.js (To hold the DraftManager and the Agents)

Create index.js in the project's root.
Go to the section of the page with this teleport and copy past all >> teleport me to the # of the code

[2] Running the Simulation

2.1 - Start the simulation by running:

   node index.js
Enter fullscreen mode Exit fullscreen mode

2.2 Watch the console output for detailed logs of the draft process.

2.3 Optional - Connect to the IRC channel to see the draft messages in real-time.

The Output

  • The console will display detailed logs of each pick, including the AI's thought process and decision-making.
  • In the IRC channel, you'll see:
    • Draft start announcement
    • Current draft state before each pick
    • Player picks and justifications
    • Final team compositions

Customization

  • Modify the ReActAgent class to change the AI's decision-making process.
  • Adjust the DraftSimulator class to alter the draft rules or process.
  • Change the generateDraftOrder method to implement different draft order strategies.

About the IRC and some configurations.

  1. IRC Server: The script is configured to use irc.libera.chat. If you want to use a different IRC server, modify the server address in the DraftSimulator constructor.

  2. Channel: The default channel is #leagueoflegends-draft. You can change this in the DraftSimulator constructor.

  3. Teams and Champion Pools: The teamBlue and teamRed arrays define the players and their champion pools. Modify these arrays to customize the teams and available champions.

Finally the code :)

I will explain the logic behind, and "optimize" the solution, with single responsibility and good practices that are inherent later on.
Sorry for so long code, but if you arrived here, it is simply copy and paste.

Stay tuned and hope you have fun playing around for now!

Copy from Here

Teleport me back to running

require("dotenv").config();
const { GoogleGenerativeAI } = require("@google/generative-ai");
const irc = require("irc");

const genAI = new GoogleGenerativeAI(process.env.GOOGLE_AI_API_KEY);

class ReActAgent {
  constructor(query, functions, championPool, ircClient, channel) {
    this.query = query;
    this.functions = new Set(functions);
    this.championPool = championPool;
    this.state = "THOUGHT";
    this._history = [];
    this.model = genAI.getGenerativeModel({
      model: "gemini-pro",
      temperature: 0.9,
    });
    this.ircClient = ircClient;
    this.channel = channel;
  }

  get history() {
    return this._history;
  }

  pushHistory(value) {
    this._history.push(`\n ${value}`);
  }

  async run(ownComp, enemyComp) {
    this.pushHistory(`**Task: ${this.query} **`);
    this.pushHistory(`Own team composition: ${JSON.stringify(ownComp)}`);
    this.pushHistory(`Enemy team composition: ${JSON.stringify(enemyComp)}`);
    try {
      return await this.step(ownComp, enemyComp);
    } catch (e) {
      console.error("Error in ReActAgent:", e);
      return "Unable to process the draft pick. Please try again.";
    }
  }

  async step(ownComp, enemyComp) {
    console.log(`Current state: ${this.state}`);
    switch (this.state) {
      case "THOUGHT":
        return await this.thought(ownComp, enemyComp);
      case "ACTION":
        return await this.action(ownComp, enemyComp);
      case "ANSWER":
        return await this.answer();
    }
  }

  async promptModel(prompt) {
    try {
      const result = await this.model.generateContent(prompt);
      const response = await result.response;
      return response.text();
    } catch (error) {
      console.error("Error in promptModel:", error);
      return "Error occurred while generating content.";
    }
  }

  async thought(ownComp, enemyComp) {
    const availableFunctions = JSON.stringify(Array.from(this.functions));
    const historyContext = this.history.join("\n");
    const prompt = `You are a League of Legends player in a draft phase. ${this.query}
Whenever there is no opponent composition, it means you are the first.
Your champion pool: ${this.championPool.join(", ")}
Your team composition: ${JSON.stringify(ownComp)}
Enemy team composition: ${JSON.stringify(enemyComp)}
Available actions: ${availableFunctions}
This is a competitive draft so champions can't be repeated.

Based on the current compositions and your champion pool, think about the best pick for your team.
If you are the first, think carefully based on your championPool what to pick in a direction to enabling your team comp.
Then, decide whether to analyze the composition further or make a champion pick.

Context: "${historyContext}"

Respond with your decision in form of a thought and whether you want to "Analyze" or "Pick".`;

    const thought = await this.promptModel(prompt);
    this.pushHistory(`\n **${thought.trim()}**`);
    if (thought.toLowerCase().includes("pick")) {
      this.state = "ANSWER";
    } else {
      this.state = "ACTION";
    }
    return await this.step(ownComp, enemyComp);
  }

  async action(ownComp, enemyComp) {
    const action = "analyzeComposition";
    this.pushHistory(`** Action: ${action} **`);
    const result = await this.analyzeComposition(ownComp, enemyComp);
    this.pushHistory(`** ActionResult: ${result} **`);
    this.state = "THOUGHT";
    return await this.step(ownComp, enemyComp);
  }

  async analyzeComposition(ownComp, enemyComp) {
    const prompt = `Analyze the following team compositions:
Own team: ${JSON.stringify(ownComp)}
Enemy team: ${JSON.stringify(enemyComp)}

Consider team synergies, counter picks, and overall strategy. Suggest potential champions from your pool that would fit well.
Your champion pool: ${this.championPool.join(", ")}

Provide a brief analysis and champion suggestions.`;

    return await this.promptModel(prompt);
  }

  async answer() {
    const historyContext = this.history.join("\n");
    const prompt = `Based on the following context and analysis, choose a champion for the draft.
Your champion pool: ${this.championPool.join(", ")}

Context: ${historyContext}

reflect(>>>FeedForward<( FlushBackL{< from the Context >>  briefly explain into  _justifiedVerify
Respond with: "I pick [ChampionName]" where [ChampionName] is the champion you've decided and the ups and downs _justifiedVerify
`;

    const finalAnswer = await this.promptModel(prompt);
    this.pushHistory(`Answer: ${finalAnswer}`);
    return finalAnswer;
  }

  say(message) {
    this.ircClient.say(this.channel, message);
  }
}

const teamBlue = [
  {
    name: "BlueTop",
    rank: "Diamond III",
    role: "Top",
    championPool: [
      "Aatrox",
      "Camille",
      "Darius",
      "Fiora",
      "Garen",
      "Gnar",
      "Irelia",
      "Jax",
      "Jayce",
      "Kennen",
      "Malphite",
      "Maokai",
      "Mordekaiser",
      "Nasus",
      "Ornn",
      "Poppy",
      "Renekton",
      "Riven",
      "Sett",
      "Shen",
      "Sion",
      "Teemo",
      "Urgot",
      "Vladimir",
      "Volibear",
    ],
  },
  {
    name: "BlueJungle",
    rank: "Platinum I",
    role: "Jungle",
    championPool: [
      "Amumu",
      "Elise",
      "Evelynn",
      "Fiddlesticks",
      "Gragas",
      "Graves",
      "Hecarim",
      "Ivern",
      "Jarvan IV",
      "Karthus",
      "Kayn",
      "Kha'Zix",
      "Kindred",
      "Lee Sin",
      "Master Yi",
      "Nidalee",
      "Nunu & Willump",
      "Olaf",
      "Rammus",
      "Rek'Sai",
      "Rengar",
      "Sejuani",
      "Shaco",
      "Trundle",
      "Udyr",
      "Vi",
      "Warwick",
      "Xin Zhao",
      "Zac",
    ],
  },
  {
    name: "BlueMid",
    rank: "Diamond II",
    role: "Mid",
    championPool: [
      "Ahri",
      "Akali",
      "Anivia",
      "Annie",
      "Aurelion Sol",
      "Azir",
      "Cassiopeia",
      "Corki",
      "Diana",
      "Ekko",
      "Fizz",
      "Galio",
      "Kassadin",
      "Katarina",
      "LeBlanc",
      "Lissandra",
      "Lux",
      "Malzahar",
      "Neeko",
      "Orianna",
      "Qiyana",
      "Ryze",
      "Sylas",
      "Syndra",
      "Talon",
      "Twisted Fate",
      "Veigar",
      "Viktor",
      "Yasuo",
      "Zed",
      "Ziggs",
      "Zoe",
    ],
  },
  {
    name: "BlueADC",
    rank: "Diamond III",
    role: "ADC",
    championPool: [
      "Aphelios",
      "Ashe",
      "Caitlyn",
      "Draven",
      "Ezreal",
      "Jhin",
      "Jinx",
      "Kai'Sa",
      "Kalista",
      "Kog'Maw",
      "Lucian",
      "Miss Fortune",
      "Senna",
      "Sivir",
      "Tristana",
      "Twitch",
      "Varus",
      "Vayne",
      "Xayah",
    ],
  },
  {
    name: "BlueSupport",
    rank: "Platinum II",
    role: "Support",
    championPool: [
      "Alistar",
      "Bard",
      "Blitzcrank",
      "Brand",
      "Braum",
      "Janna",
      "Karma",
      "Leona",
      "Lulu",
      "Morgana",
      "Nami",
      "Nautilus",
      "Pyke",
      "Rakan",
      "Senna",
      "Sona",
      "Soraka",
      "Swain",
      "Tahm Kench",
      "Taric",
      "Thresh",
      "Vel'Koz",
      "Xerath",
      "Yuumi",
      "Zilean",
      "Zyra",
    ],
  },
];

const teamRed = [
  {
    name: "RedTop",
    rank: "Diamond II",
    role: "Top",
    championPool: [
      "Aatrox",
      "Camille",
      "Cho'Gath",
      "Darius",
      "Dr. Mundo",
      "Fiora",
      "Gangplank",
      "Garen",
      "Gnar",
      "Illaoi",
      "Irelia",
      "Jax",
      "Jayce",
      "Kayle",
      "Kennen",
      "Kled",
      "Malphite",
      "Maokai",
      "Mordekaiser",
      "Nasus",
      "Ornn",
      "Pantheon",
      "Poppy",
      "Quinn",
      "Renekton",
      "Riven",
      "Rumble",
      "Ryze",
      "Sett",
      "Shen",
      "Singed",
      "Sion",
      "Teemo",
      "Tryndamere",
      "Urgot",
      "Vladimir",
      "Volibear",
      "Wukong",
      "Yorick",
    ],
  },
  {
    name: "RedJungle",
    rank: "Diamond I",
    role: "Jungle",
    championPool: [
      "Amumu",
      "Elise",
      "Evelynn",
      "Fiddlesticks",
      "Gragas",
      "Graves",
      "Hecarim",
      "Ivern",
      "Jarvan IV",
      "Karthus",
      "Kayn",
      "Kha'Zix",
      "Kindred",
      "Lee Sin",
      "Master Yi",
      "Nidalee",
      "Nocturne",
      "Nunu & Willump",
      "Olaf",
      "Rammus",
      "Rek'Sai",
      "Rengar",
      "Sejuani",
      "Shaco",
      "Skarner",
      "Taliyah",
      "Trundle",
      "Udyr",
      "Vi",
      "Warwick",
      "Xin Zhao",
      "Zac",
    ],
  },
  {
    name: "RedMid",
    rank: "Master",
    role: "Mid",
    championPool: [
      "Ahri",
      "Akali",
      "Anivia",
      "Annie",
      "Aurelion Sol",
      "Azir",
      "Cassiopeia",
      "Corki",
      "Diana",
      "Ekko",
      "Fizz",
      "Galio",
      "Irelia",
      "Kassadin",
      "Katarina",
      "LeBlanc",
      "Lissandra",
      "Lux",
      "Malzahar",
      "Neeko",
      "Orianna",
      "Pantheon",
      "Qiyana",
      "Rumble",
      "Ryze",
      "Sylas",
      "Syndra",
      "Talon",
      "Twisted Fate",
      "Veigar",
      "Viktor",
      "Xerath",
      "Yasuo",
      "Zed",
      "Ziggs",
      "Zoe",
    ],
  },
  {
    name: "RedADC",
    rank: "Diamond II",
    role: "ADC",
    championPool: [
      "Aphelios",
      "Ashe",
      "Caitlyn",
      "Draven",
      "Ezreal",
      "Jhin",
      "Jinx",
      "Kai'Sa",
      "Kalista",
      "Kog'Maw",
      "Lucian",
      "Miss Fortune",
      "Senna",
      "Sivir",
      "Tristana",
      "Twitch",
      "Varus",
      "Vayne",
      "Xayah",
    ],
  },
  {
    name: "RedSupport",
    rank: "Diamond III",
    role: "Support",
    championPool: [
      "Alistar",
      "Bard",
      "Blitzcrank",
      "Brand",
      "Braum",
      "Janna",
      "Karma",
      "Leona",
      "Lulu",
      "Morgana",
      "Nami",
      "Nautilus",
      "Pyke",
      "Rakan",
      "Senna",
      "Sona",
      "Soraka",
      "Swain",
      "Tahm Kench",
      "Taric",
      "Thresh",
      "Vel'Koz",
      "Xerath",
      "Yuumi",
      "Zilean",
      "Zyra",
    ],
  },
];

class DraftSimulator {
  constructor() {
    this.channel = "#leagueoflegends-draft";
    this.blueTeam = this.createTeamAgents(teamBlue, "Blue");
    this.redTeam = this.createTeamAgents(teamRed, "Red");
    this.allPlayers = [...this.blueTeam, ...this.redTeam];
    this.draftOrder = this.generateDraftOrder();
    this.currentPickIndex = 0;
    this.blueComp = {
      Top: null,
      Jungle: null,
      Mid: null,
      ADC: null,
      Support: null,
    };
    this.redComp = {
      Top: null,
      Jungle: null,
      Mid: null,
      ADC: null,
      Support: null,
    };
    this.draftHistory = [];

    this.moderatorClient = new irc.Client("irc.libera.chat", "DraftModerator", {
      channels: [this.channel],
      port: 6667,
      autoRejoin: true,
      retryCount: 3,
      debug: false,
    });

    this.setupIRCListeners();
  }

  createTeamAgents(teamData, teamColor) {
    return teamData.map((player) => {
      const query = `As ${player.name}, a ${player.rank} ${player.role} player for team ${teamColor}, choose a champion for the current draft.`;
      const functions = [
        [
          "analyzeComposition",
          "params: ownComp, enemyComp",
          "Analyze the current team compositions",
        ],
      ];
      const ircClient = new irc.Client("irc.libera.chat", player.name, {
        channels: [this.channel],
        port: 6667,
        autoRejoin: true,
        retryCount: 3,
        debug: false,
      });
      return {
        role: player.role,
        agent: new ReActAgent(
          query,
          functions,
          player.championPool,
          ircClient,
          this.channel,
        ),
      };
    });
  }

  generateDraftOrder() {
    const order = [];
    const blueRoles = ["Top", "Jungle", "Mid", "ADC", "Support"];
    const redRoles = ["Top", "Jungle", "Mid", "ADC", "Support"];

    const pickForTeam = (team, count) => {
      const roles = team === "Blue" ? blueRoles : redRoles;
      for (let i = 0; i < count; i++) {
        if (roles.length > 0) {
          const role = this.getRandomRole(roles);
          order.push({ team, role });
        }
      }
    };

    // Blue team always first-picks
    pickForTeam("Blue", 1);

    // Red team picks two
    pickForTeam("Red", 2);

    // Blue team picks two
    pickForTeam("Blue", 2);

    // Red team picks two
    pickForTeam("Red", 2);

    // Blue team picks two
    pickForTeam("Blue", 2);

    // Red team last-picks
    pickForTeam("Red", 1);

    return order;
  }

  getRandomRole(roles) {
    const index = Math.floor(Math.random() * roles.length);
    const role = roles[index];
    roles.splice(index, 1);
    return role;
  }

  setupIRCListeners() {
    this.moderatorClient.addListener("error", this.handleIrcError.bind(this));
    this.moderatorClient.addListener(
      "registered",
      this.handleRegistered.bind(this),
    );
    this.moderatorClient.addListener("join", this.handleJoin.bind(this));
    this.moderatorClient.addListener(
      "message" + this.channel,
      this.handleMessage.bind(this),
    );
  }

  handleIrcError(message) {
    console.error("IRC Error:", message);
  }

  handleRegistered(message) {
    console.log("Registered with server:", message);
  }

  handleJoin(channel, nick, message) {
    console.log(`Joined ${channel} as ${nick}`);
    if (channel === this.channel && nick === this.moderatorClient.nick) {
      console.log("Moderator joined the correct channel, starting draft...");
      setTimeout(() => this.startDraft(), 1000);
    }
  }

  handleMessage(from, message) {
    console.log(`Message received in ${this.channel}: ${from} => ${message}`);
  }

  async startDraft() {
    console.log("Starting draft...");
    this.moderatorClient.say(this.channel, "Draft is starting!");
    setTimeout(() => this.nextPick(), 1000);
  }

  async nextPick() {
    console.log(`Current pick index: ${this.currentPickIndex}`);
    if (this.currentPickIndex >= this.draftOrder.length) {
      this.endDraft();
      return;
    }

    const currentPick = this.draftOrder[this.currentPickIndex];
    const currentTeam = currentPick.team;
    const currentRole = currentPick.role;
    const teamPlayers = currentTeam === "Blue" ? this.blueTeam : this.redTeam;

    console.log(`Current team: ${currentTeam}, Current role: ${currentRole}`);

    // Announce current state through the moderator
    this.announceDraftState();

    const currentPlayer = teamPlayers.find((p) => p.role === currentRole);

    if (!currentPlayer) {
      console.error(
        `No player found for role ${currentRole} in ${currentTeam} team`,
      );
      this.moderatorClient.say(
        this.channel,
        `Error: No player found for ${currentRole} in ${currentTeam} team. Skipping this pick.`,
      );
      this.currentPickIndex++;
      setTimeout(() => this.nextPick(), 2000);
      return;
    }

    const ownComp = currentTeam === "Blue" ? this.blueComp : this.redComp;
    const enemyComp = currentTeam === "Blue" ? this.redComp : this.blueComp;

    console.log(`Current player: ${currentPlayer.agent.query}`);

    try {
      const result = await currentPlayer.agent.run(ownComp, enemyComp);
      const champion = this.extractChampionFromResult(result);

      console.log(`Pick result: ${result}`);
      console.log(`Extracted champion: ${champion}`);

      if (champion === "Unknown Champion") {
        throw new Error("Failed to extract a valid champion from the result");
      }

      this.processPick(currentTeam, currentRole, champion);
      currentPlayer.agent.say(
        `I pick ${champion} for ${currentRole}. Justified with: ${result}`,
      );

      // Announce the pick through the moderator
      this.moderatorClient.say(
        this.channel,
        `${currentPlayer.agent.query.split(",")[0]} has picked ${champion} for ${currentRole}.`,
      );

      this.currentPickIndex++;
      setTimeout(() => this.nextPick(), 7000);
    } catch (error) {
      console.error("Error during pick:", error);
      this.moderatorClient.say(
        this.channel,
        `An error occurred during the pick. ${currentPlayer.agent.query.split(",")[0]} will be assigned a random champion from their pool.`,
      );
      const randomChampion = this.getRandomChampion(
        currentPlayer.agent.championPool,
      );
      this.processPick(currentTeam, currentRole, randomChampion);
      this.moderatorClient.say(
        this.channel,
        `${currentPlayer.agent.query.split(",")[0]} has been assigned ${randomChampion} for ${currentRole}.`,
      );
      this.currentPickIndex++;
      setTimeout(() => this.nextPick(), 7000);
    }
  }

  extractChampionFromResult(result) {
    // First, try to match "I pick [ChampionName]"
    let match = result.match(/I pick (\w+)/i);
    if (match) return match[1];

    // If that fails, look for any champion name in bold
    match = result.match(/\*\*(\w+)\*\*/);
    if (match) return match[1];

    // If still no match, search for any word that matches a champion in the pool
    const words = result.split(/\s+/);
    for (const word of words) {
      const champion = word.replace(/[^a-zA-Z']/g, ""); // Remove non-alphabetic characters except apostrophe
      if (this.isValidChampion(champion)) {
        return champion;
      }
    }

    return "Unknown Champion";
  }

  isValidChampion(champion) {
    const allChampions = new Set([
      ...teamBlue.flatMap((player) => player.championPool),
      ...teamRed.flatMap((player) => player.championPool),
    ]);
    return allChampions.has(champion);
  }

  getRandomChampion(championPool) {
    return championPool[Math.floor(Math.random() * championPool.length)];
  }

  announceDraftState() {
    const blueTeamComp = Object.entries(this.blueComp)
      .map(([role, champion]) => `${role}: ${champion || "Not picked"}`)
      .join(", ");
    const redTeamComp = Object.entries(this.redComp)
      .map(([role, champion]) => `${role}: ${champion || "Not picked"}`)
      .join(", ");

    const currentPick = this.draftOrder[this.currentPickIndex];

    this.moderatorClient.say(
      this.channel,
      `Current Draft State:
      Blue Team: ${blueTeamComp}
      Red Team: ${redTeamComp}
      It's ${currentPick.team} team's turn to pick for ${currentPick.role}.`,
    );
  }

  processPick(team, role, champion) {
    if (team === "Blue") {
      this.blueComp[role] = champion;
    } else {
      this.redComp[role] = champion;
    }
    this.draftHistory.push(`${team} ${role}: ${champion}`);
  }

  endDraft() {
    console.log("Draft completed!");
    console.log("Blue Team Composition:", this.blueComp);
    console.log("Red Team Composition:", this.redComp);
    this.moderatorClient.say(
      this.channel,
      "Draft completed! Final compositions:",
    );
    setTimeout(() => {
      this.moderatorClient.say(
        this.channel,
        `Blue Team: ${JSON.stringify(this.blueComp)}`,
      );
    }, 1000);
    setTimeout(() => {
      this.moderatorClient.say(
        this.channel,
        `Red Team: ${JSON.stringify(this.redComp)}`,
      );
    }, 2000);

    setTimeout(() => {
      this.allPlayers.forEach((player) => player.agent.ircClient.disconnect());
      this.moderatorClient.disconnect("Draft completed", () => {
        console.log("Disconnected from IRC");
        process.exit(0);
      });
    }, 5000);
  }
}

async function main() {
  try {
    const simulator = new DraftSimulator();
    // The draft will start automatically when connected to IRC
  } catch (error) {
    console.error("Error in main:", error);
  }
}

main().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

  • If you encounter IRC connection issues, ensure your network allows IRC connections.
  • For API errors, verify your Google AI API key and check your quota limits.

Note

This simulation is for educational and entertainment purposes. It demonstrates the use of AI agents in a game-theoretic scenario and showcases integration with
IRC for multi-agent communication.
Also this is the Rea

Top comments (0)