Piszemy matchmaking dla Dota 2014

Cześć wszystkim

Tej wiosny natknąłem się na projekt, w którym chłopaki nauczyli się uruchamiać serwer Dota 2 w wersji 2014 i odpowiednio na nim grać. Jestem wielkim fanem tej gry i nie mogłem przepuścić tej wyjątkowej okazji, aby zanurzyć się w dzieciństwie.

Zanurkowałem bardzo głęboko i tak się złożyło, że napisałem bota na Discordzie, który odpowiada za prawie całą funkcjonalność, która nie jest obsługiwana w starej wersji gry, czyli matchmaking.
Przed wszystkimi innowacjami związanymi z botem lobby było tworzone ręcznie. Zebraliśmy 10 reakcji na wiadomość i ręcznie zmontowaliśmy serwer lub hostowaliśmy lokalne lobby.

Piszemy matchmaking dla Dota 2014

Moja natura jako programisty nie wytrzymała tak dużej pracy ręcznej i z dnia na dzień naszkicowałem najprostszą wersję bota, który automatycznie podnosił serwer, gdy było 10 osób.

Od razu zdecydowałem się na pisanie w nodejs, bo nie do końca przepadam za Pythonem, a w tym środowisku czuję się bardziej komfortowo.

To moje pierwsze doświadczenie z pisaniem bota dla Discorda, ale okazało się to bardzo proste. Oficjalny moduł npm discord.js zapewnia wygodny interfejs do pracy z wiadomościami, zbierania reakcji itp.

Zastrzeżenie: Wszystkie przykłady kodu są „aktualne”, co oznacza, że ​​przeszły kilka iteracji przepisywania w nocy.

Podstawą dobierania graczy jest „kolejka”, w której gracze chcący zagrać są umieszczani i usuwani, gdy nie chcą lub nie znajdują gry.

Tak wygląda istota „gracza”. Początkowo był to tylko identyfikator użytkownika na Discordzie, ale w planach jest uruchamianie/wyszukiwanie gier na stronie, ale przede wszystkim.

export enum Realm {
  DISCORD,
  EXTERNAL,
}

export default class QueuePlayer {
  constructor(public readonly realm: Realm, public readonly id: string) {}

  public is(qp: QueuePlayer): boolean {
    return this.realm === qp.realm && this.id === qp.id;
  }

  static Discord(id: string) {
    return new QueuePlayer(Realm.DISCORD, id);
  }

  static External(id: string) {
    return new QueuePlayer(Realm.EXTERNAL, id);
  }
}

A oto interfejs kolejki. Tutaj zamiast „graczy” użyto abstrakcji w postaci „grupy”. W przypadku pojedynczego gracza grupa składa się z niego samego, a w przypadku graczy w grupie odpowiednio ze wszystkich graczy w grupie.

export default interface IQueue extends EventEmitter {
  inQueue: QueuePlayer[]
  put(uid: Party): boolean;
  remove(uid: Party): boolean;
  removeAll(ids: Party[]): void;

  mode: MatchmakingMode
  roomSize: number;
  clear(): void
}

Zdecydowałem się wykorzystać zdarzenia do wymiany kontekstu. Nadawało się to do przypadków - w przypadku zdarzenia „znaleziono grę na 10 osób” można wysłać graczom niezbędną wiadomość w wiadomościach prywatnych i przeprowadzić podstawową logikę biznesową - uruchomić zadanie sprawdzenia gotowości, przygotować lobby do startu i tak dalej.

Do IOC używam InversifyJS. Mam miłe doświadczenia ze współpracą z tą biblioteką. Szybko i łatwo!

Mamy kilka kolejek na naszym serwerze - dodaliśmy tryby 1x1, normalny/oceniony i kilka niestandardowych. Dlatego istnieje pojedyncza usługa RoomService, która leży pomiędzy użytkownikiem a wyszukiwarką gry.

constructor(
    @inject(GameServers) private gameServers: GameServers,
    @inject(MatchStatsService) private stats: MatchStatsService,
    @inject(PartyService) private partyService: PartyService
  ) {
    super();
    this.initQueue(MatchmakingMode.RANKED);
    this.initQueue(MatchmakingMode.UNRANKED);
    this.initQueue(MatchmakingMode.SOLOMID);
    this.initQueue(MatchmakingMode.DIRETIDE);
    this.initQueue(MatchmakingMode.GREEVILING);
    this.partyService.addListener(
      "party-update",
      (event: PartyUpdatedEvent) => {
        this.queues.forEach((q) => {
          if (has(q.queue, (t) => t.is(event.party))) {
            // if queue has this party, we re-add party
            this.leaveQueue(event.qp, q.mode)
            this.enterQueue(event.qp, q.mode)
          }
        });
      }
    );

    this.partyService.addListener(
      "party-removed",
      (event: PartyUpdatedEvent) => {
        this.queues.forEach((q) => {
          if (has(q.queue, (t) => t.is(event.party))) {
            // if queue has this party, we re-add party
            q.remove(event.party)
          }
        });
      }
    );
  }

(Zakoduj makaron, aby dać wyobrażenie o tym, jak z grubsza wyglądają procesy)

Tutaj inicjuję kolejkę dla każdego z zaimplementowanych trybów gry, a także nasłuchuję zmian w „grupach”, aby dostosować kolejki i uniknąć pewnych konfliktów.

No to dobra robota, wstawiłem fragmenty kodu nie mające nic wspólnego z tematem, a teraz przejdźmy od razu do matchmakingu.

Rozważmy przypadek:

1) Użytkownik chce zagrać.

2) Aby rozpocząć wyszukiwanie, używa Gateway=Discord, czyli umieszcza reakcję na wiadomość:

Piszemy matchmaking dla Dota 2014

3) Ta bramka przechodzi do RoomService i mówi: „Użytkownik z niezgody chce wejść do kolejki, tryb: gra bez oceny”.

4) RoomService przyjmuje żądanie bramki i wypycha użytkownika (a dokładniej grupę użytkowników) do żądanej kolejki.

5) Kolejka jest sprawdzana za każdym razem, gdy jest wystarczająca liczba graczy do gry. Jeśli to możliwe, wyemituj zdarzenie:

private onRoomFound(players: Party[]) {
    this.emit("room-found", {
      players,
    });
  }

6) RoomService oczywiście z radością wysłuchuje każdej kolejki w niecierpliwym oczekiwaniu na to wydarzenie. Jako dane wejściowe otrzymujemy listę graczy, tworzymy od nich wirtualny „pokój” i oczywiście wydajemy wydarzenie:

queue.addListener("room-found", (event: RoomFoundEvent) => {
      console.log(
        `Room found mode: [${mode}]. Time to get free room for these guys`
      );
      const room = this.getFreeRoom(mode);
      room.fill(event.players);

      this.onRoomFormed(room);
    });

7) I tak dotarliśmy do „najwyższego” autorytetu – klasy Bot. Ogólnie zajmuje się powiązaniem bramek (nie rozumiem, jak śmiesznie to wygląda po rosyjsku) i logiką biznesową kojarzeń. Bot podsłuchuje wydarzenie i nakazuje DiscordGateway wysłanie kontroli gotowości do wszystkich użytkowników.

Piszemy matchmaking dla Dota 2014

8) Jeśli ktoś w ciągu 3 minut odrzuci lub nie zaakceptuje gry, wówczas NIE zwracamy go do kolejki. Wszystkich pozostałych zwracamy do kolejki i czekamy, aż znów będzie 10 osób. Jeśli wszyscy gracze zaakceptują grę, rozpoczyna się interesująca część.

Dedykowana konfiguracja serwera

Nasze gry hostowane są na VDS z systemem Windows Server 2012. Z tego możemy wyciągnąć kilka wniosków:

  1. Nie ma na nim żadnego dokera, co mnie uderzyło w serce
  2. Oszczędzamy na czynszu

Zadanie polega na uruchomieniu procesu na VDS z VPS na Linuksie. Napisałem prosty serwer w Flasku. Tak, nie lubię Pythona, ale co zrobić? Szybciej i łatwiej jest napisać na nim ten serwer.

Pełni 3 funkcje:

  1. Uruchomienie serwera wraz z konfiguracją - wybranie mapy, liczby graczy, dla których rozpocznie się gra oraz zestawu wtyczek. O wtyczkach nie będę się już rozpisywać – to inna historia z litrami kawy wieczorem wymieszanej ze łzami i potarganymi włosami.
  2. Zatrzymanie/restart serwera w przypadku nieudanych połączeń, z którymi możemy sobie poradzić jedynie ręcznie.

Tutaj wszystko jest proste, przykłady kodu nie są nawet odpowiednie. Skrypt 100-liniowy

Tak więc, gdy zebrało się 10 osób i zaakceptowało grę, serwer został uruchomiony i wszyscy byli chętni do gry, w prywatnych wiadomościach został wysłany link umożliwiający połączenie się z grą.

Piszemy matchmaking dla Dota 2014

Klikając na link, gracz łączy się z serwerem gry i to wszystko. Po około 25 minutach wirtualny „pokój” z graczami zostaje oczyszczony.

Z góry przepraszam za niezręczność artykułu, dawno tu nie pisałem, a kodu jest za dużo, żeby wyróżnić ważne sekcje. W skrócie makaron.

Jeśli zobaczę zainteresowanie tematem, pojawi się druga część - będzie zawierała moją udrękę z wtyczkami do srcds (serwer dedykowany źródłowy), oraz prawdopodobnie system oceniania i mini-dotabuff, czyli stronę ze statystykami gier.

Niektóre linki:

  1. Nasza strona internetowa (statystyki, tabela wyników, mała strona docelowa i pobieranie klienta)
  2. Serwer Discord

Źródło: www.habr.com

Dodaj komentarz