ჩვენ ვწერთ Matchmaking-ს Dota 2014-ისთვის

გაუმარჯოს ყველას.

Этой весной я наткнулся на проект, в котором ребята научились запускать Dota 2 сервер версии 2014 года и, соответственно, играть на нем. Я большой фанат этой игры, и не смог пройти мимо уникальной возможности окунуться в свое детство.

ძალიან ღრმად ჩავვარდი და ისე მოხდა, რომ დავწერე Discord ბოტი, რომელიც პასუხისმგებელია თითქმის ყველა ფუნქციონალურობაზე, რომელიც არ არის მხარდაჭერილი თამაშის ძველ ვერსიაში, კერძოდ matchmaking-ზე.
ბოტის ყველა სიახლემდე, ლობი ხელით შეიქმნა. ჩვენ შევაგროვეთ 10 რეაქცია შეტყობინებაზე და ხელით შევკრიბეთ სერვერი, ან ვმასპინძლოთ ადგილობრივ ლობიში.

ჩვენ ვწერთ Matchmaking-ს Dota 2014-ისთვის

ჩემი, როგორც პროგრამისტის ბუნება ვერ გაუძლო ამხელა ხელით მუშაობას და ღამით მე გამოვხატე ბოტის უმარტივესი ვერსია, რომელიც ავტომატურად აამაღლებდა სერვერს, როცა 10 ადამიანი იყო.

მაშინვე გადავწყვიტე დამეწერა nodejs-ში, რადგან პითონი ნამდვილად არ მომწონს და ამ გარემოში თავს უფრო კომფორტულად ვგრძნობ.

Это мой первый опыт написания бота для Discord, но оказалось все очень даже просто. Официальный npm модуль discord.js предоставляет удобный интерфейс для работы с сообщениями, сбором реакций и т.д.

Дисклеймер: все примеры кода являются «актуальными», то есть прошли несколько итераций переписывания по ночам.

მატჩის საფუძველს წარმოადგენს "რიგი", რომელშიც მოთამაშეები, რომლებსაც უნდათ თამაში, მოთავსებულნი არიან და აშორებენ, როდესაც მათ არ სურთ ან იპოვონ თამაში.

ასე გამოიყურება "მოთამაშის" არსი. თავდაპირველად ეს იყო მხოლოდ მომხმარებლის ID Discord-ში, მაგრამ არსებობს გეგმები საიტიდან თამაშების გაშვება/მოძიება, მაგრამ პირველ რიგში.

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);
  }
}

და აქ არის რიგის ინტერფეისი. აქ „მოთამაშეების“ ნაცვლად გამოიყენება აბსტრაქცია „ჯგუფის“ სახით. ერთი მოთამაშისთვის ჯგუფი შედგება საკუთარი თავისგან, ხოლო ჯგუფის მოთამაშეებისთვის, შესაბამისად, ჯგუფის ყველა მოთამაშისგან.

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
}

Решил использовать события для обмена контекстом. Подходило под кейсы — по событию «найдена игра для 10 человек» можно и отправить в личные сообщения игрокам нужное сообщение, и выполнить основную бизнес логику — запустить таск для проверки готовности, подготовить лобби к запуску и так далее.

Для IOC я использую InversifyJS. Имею приятный опыт работы с этой библиотекой. Быстро и просто!

Очередей у нас на сервере несколько — добавились режими 1х1, обычный/рейтинговый, и пара кастомок. Поэтому есть singleton RoomService, который лежит между пользователем и поиском игры.

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)
          }
        });
      }
    );
  }

(დაწერეთ noodles, რათა წარმოიდგინოთ, როგორ გამოიყურება დაახლოებით ეს პროცესები)

Здесь я инициализирую очередь под каждый из реализованных режимов игры, а так же слушаю изменения «групп», чтобы подкорректировать очереди и избежать некоторых конфликтов.

ჰოდა, კარგი, ჩავდე კოდის ნაწილები, რომლებიც თემასთან არაფერ შუაშია და ახლა პირდაპირ მაჭანკლზე გადავიდეთ.

განვიხილოთ შემთხვევა:

1) მომხმარებელს სურს თამაში.

2) ძიების დასაწყებად იყენებს Gateway=Discord, ანუ აყენებს რეაქციას შეტყობინებაზე:

ჩვენ ვწერთ Matchmaking-ს Dota 2014-ისთვის

3) ეს კარიბჭე მიდის RoomService-ზე და ამბობს: „უთანხმოების მომხმარებელს სურს შევიდეს რიგში, რეჟიმი: არარეიტინგული თამაში“.

4) RoomService იღებს კარიბჭის მოთხოვნას და უბიძგებს მომხმარებელს (უფრო ზუსტად, მომხმარებელთა ჯგუფს) სასურველ რიგში.

5) რიგი ამოწმებს ყოველ ჯერზე, როცა საკმარისი მოთამაშეა სათამაშოდ. თუ შესაძლებელია, გამოაქვეყნეთ მოვლენა:

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

6) RoomService აშკარად სიამოვნებით უსმენს ყველა რიგს ამ მოვლენის შეშფოთებული მოლოდინში. ჩვენ ვიღებთ მოთამაშეთა სიას შეყვანის სახით, ვქმნით მათგან ვირტუალურ „ოთახს“ და, რა თქმა უნდა, ვაძლევთ ღონისძიებას:

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) Вот мы и добрались до «высшей» инстанции — класса Bot. ზოგადად, ის ეხება კარიბჭეების კავშირს (ვერ ვხვდები, რამდენად სასაცილოა რუსულად) და მაჭანკლობის ბიზნეს ლოგიკას შორის. ბოტი ისმენს მოვლენას და ბრძანებს DiscordGateway-ს გაუგზავნოს მზადყოფნის შემოწმება ყველა მომხმარებლისთვის.

ჩვენ ვწერთ Matchmaking-ს Dota 2014-ისთვის

8) თუ ვინმე უარს იტყვის ან არ მიიღებს თამაშს 3 წუთის განმავლობაში, მაშინ ჩვენ არ ვაბრუნებთ მათ რიგში. ყველა დანარჩენს ვაბრუნებთ რიგში და ველოდებით სანამ ისევ 10 ადამიანი იქნება. თუ ყველა მოთამაშემ მიიღო თამაში, მაშინ იწყება საინტერესო ნაწილი.

Конфигурация выделенного сервера

У нас игры хостятся на VDS c Windows server 2012. Из этого можно сделать несколько выводов:

  1. მასზე არ არის დოკერი, რომელიც გულში მომხვდა
  2. Мы экономим на аренде

ამოცანაა VDS-ზე პროცესის გაშვება VPS-დან Linux-ზე. მე დავწერე მარტივი სერვერი Flask-ში. დიახ, მე არ მომწონს პითონი, მაგრამ რა შეგიძლიათ გააკეთოთ? უფრო სწრაფი და ადვილია მასზე ამ სერვერის დაწერა.

ის ასრულებს 3 ფუნქციას:

  1. სერვერის გაშვება კონფიგურაციით - რუკის შერჩევა, თამაშის დასაწყებად მოთამაშეთა რაოდენობა და დანამატების ნაკრები. ახლა არ დავწერ დანამატებზე - ეს სხვა ამბავია ღამით ლიტრი ყავით შერეული ცრემლებითა და დახეული თმებით.
  2. Остановка/перезапуск сервера в случае неудачных подключений, которые мы можем обработать только вручную.

აქ ყველაფერი მარტივია, კოდის მაგალითებიც კი არ არის შესაბამისი. 100 სტრიქონიანი სკრიპტი

Итак, когда 10 человек собрались вместе и приняли игру, запущен сервер и все жаждут играть, в личные сообщения приходит ссылка на подключение к игре.

ჩვენ ვწერთ Matchmaking-ს Dota 2014-ისთვის

ბმულზე დაწკაპუნებით, მოთამაშე უერთდება თამაშის სერვერს და ეს არის ის. ~25 წუთის შემდეგ, ვირტუალური "ოთახი" მოთამაშეებით გაწმენდილია.

წინასწარ ბოდიშს ვიხდი სტატიის უხერხულობისთვის, დიდი ხანია აქ არ დამიწერია და ძალიან ბევრი კოდია მნიშვნელოვანი სექციების ხაზგასასმელად. ნუდლი, მოკლედ.

თუ თემის მიმართ ინტერესს დავინახავ, იქნება მეორე ნაწილი - ის შეიცავს ჩემს ტანჯვას დანამატებით srcds-სთვის (Source dedicated server) და, ალბათ, რეიტინგის სისტემა და მინი-დოტაბუფი, საიტი თამაშის სტატისტიკით.

Немного ссылок:

  1. ჩვენი ვებსაიტი (სტატისტიკა, ლიდერბორდი, მცირე სადესანტო გვერდი და კლიენტის ჩამოტვირთვა)
  2. Discord სერვერი

წყარო: www.habr.com

ახალი კომენტარის დამატება