Γράψιμο matchmaking για το Dota 2014

Γεια σε όλους.

Αυτή την άνοιξη συνάντησα ένα έργο στο οποίο τα παιδιά έμαθαν πώς να τρέχουν τον διακομιστή Dota 2 έκδοση 2014 και, κατά συνέπεια, να παίζουν σε αυτόν. Είμαι μεγάλος θαυμαστής αυτού του παιχνιδιού και δεν θα μπορούσα να χάσω αυτή τη μοναδική ευκαιρία να βυθιστώ στην παιδική μου ηλικία.

Περιστερίστηκα πολύ βαθιά και έτυχε να έγραψα ένα bot Discord που είναι υπεύθυνο για σχεδόν όλη τη λειτουργικότητα που δεν υποστηρίζεται στην παλιά έκδοση του παιχνιδιού, δηλαδή το matchmaking.
Πριν από όλες τις καινοτομίες με το bot, το λόμπι δημιουργήθηκε χειροκίνητα. Συλλέξαμε 10 αντιδράσεις σε ένα μήνυμα και δημιουργήσαμε χειροκίνητα έναν διακομιστή ή φιλοξενήσαμε ένα τοπικό λόμπι.

Γράψιμο matchmaking για το Dota 2014

Η φύση μου ως προγραμματιστής δεν μπορούσε να αντέξει τόση χειρωνακτική εργασία και μέσα σε μια νύχτα σκιαγράφησα την απλούστερη έκδοση του bot, που ανέβαζε αυτόματα τον διακομιστή όταν υπήρχαν 10 άτομα.

Αποφάσισα αμέσως να γράψω σε nodejs, γιατί δεν μου αρέσει πολύ η Python και νιώθω πιο άνετα σε αυτό το περιβάλλον.

Αυτή είναι η πρώτη μου εμπειρία γράφοντας ένα bot για το Discord, αλλά αποδείχθηκε πολύ απλό. Η επίσημη μονάδα npm discord.js παρέχει μια βολική διεπαφή για εργασία με μηνύματα, συλλογή αντιδράσεων κ.λπ.

Αποποίηση ευθύνης: Όλα τα παραδείγματα κώδικα είναι "τρέχοντα", που σημαίνει ότι έχουν περάσει από πολλές επαναλήψεις επανεγγραφής τη νύχτα.

Η βάση του matchmaking είναι μια «ουρά» στην οποία οι παίκτες που θέλουν να παίξουν τοποθετούνται και απομακρύνονται όταν δεν θέλουν ή δεν βρίσκουν ένα παιχνίδι.

Έτσι μοιάζει η ουσία ενός «παίχτη». Αρχικά ήταν απλώς ένα αναγνωριστικό χρήστη στο 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 άτομα", μπορείτε να στείλετε το απαραίτητο μήνυμα στους παίκτες σε προσωπικά μηνύματα και να εκτελέσετε τη βασική επιχειρηματική λογική - ξεκινήστε μια εργασία για να ελέγξετε την ετοιμότητα, να προετοιμάσετε το λόμπι για εκτόξευση και ούτω καθεξής.

Για τη ΔΟΕ χρησιμοποιώ το InversifyJS. Έχω μια ευχάριστη εμπειρία εργασίας με αυτήν τη βιβλιοθήκη. Γρήγορα και εύκολα!

Έχουμε πολλές ουρές στον διακομιστή μας - έχουμε προσθέσει 1x1, κανονικό/βαθμολογημένο και μερικές προσαρμοσμένες λειτουργίες. Επομένως, υπάρχει ένα 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 για να δώσετε μια ιδέα για το πώς μοιάζουν περίπου οι διαδικασίες)

Εδώ αρχικοποιώ την ουρά για κάθε έναν από τους τρόπους παιχνιδιού που έχουν υλοποιηθεί και επίσης ακούω αλλαγές σε "ομάδες" για να προσαρμόσω τις ουρές και να αποφύγω κάποιες διενέξεις.

Λοιπόν, μπράβο, έβαλα κομμάτια κώδικα που δεν έχουν καμία σχέση με το θέμα και ας περάσουμε κατευθείαν στο matchmaking.

Ας εξετάσουμε την περίπτωση:

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. Γενικά, ασχολείται με τη σύνδεση μεταξύ των πυλών (δεν μπορώ να καταλάβω πόσο αστείο φαίνεται στα ρωσικά) και την επιχειρηματική λογική του matchmaking. Το bot κρυφακούει το συμβάν και δίνει εντολή στο DiscordGateway να στείλει έλεγχο ετοιμότητας σε όλους τους χρήστες.

Γράψιμο matchmaking για το Dota 2014

8) Αν κάποιος απορρίψει ή δεν δεχτεί το παιχνίδι μέσα σε 3 λεπτά, τότε ΔΕΝ τον επιστρέφουμε στην ουρά. Επιστρέφουμε όλους τους άλλους στην ουρά και περιμένουμε μέχρι να μείνουν ξανά 10 άτομα. Αν όλοι οι παίκτες έχουν αποδεχθεί το παιχνίδι, τότε αρχίζει το ενδιαφέρον κομμάτι.

Διαμόρφωση αποκλειστικού διακομιστή

Τα παιχνίδια μας φιλοξενούνται σε VDS με Windows server 2012. Από αυτό μπορούμε να βγάλουμε αρκετά συμπεράσματα:

  1. Δεν υπάρχει αποβάθρα σε αυτό, που με χτύπησε στην καρδιά
  2. Κάνουμε οικονομία στο ενοίκιο

Το καθήκον είναι να εκτελέσετε μια διαδικασία σε VDS από ένα VPS σε Linux. Έγραψα έναν απλό διακομιστή στο Flask. Ναι, δεν μου αρέσει η Python, αλλά τι μπορείτε να κάνετε; Είναι πιο γρήγορο και πιο εύκολο να γράψετε αυτόν τον διακομιστή σε αυτόν.

Εκτελεί 3 λειτουργίες:

  1. Εκκίνηση διακομιστή με διαμόρφωση - επιλογή χάρτη, αριθμός παικτών που θα ξεκινήσουν το παιχνίδι και ένα σύνολο προσθηκών. Δεν θα γράψω για τα πρόσθετα τώρα - αυτή είναι μια διαφορετική ιστορία με λίτρα καφέ τη νύχτα ανακατεμένα με δάκρυα και σκισμένα μαλλιά.
  2. Διακοπή/επανεκκίνηση του διακομιστή σε περίπτωση ανεπιτυχών συνδέσεων, τις οποίες μπορούμε να χειριστούμε μόνο χειροκίνητα.

Όλα είναι απλά εδώ, τα παραδείγματα κώδικα δεν είναι καν κατάλληλα. Σενάριο 100 γραμμών

Έτσι, όταν 10 άτομα συγκεντρώθηκαν και αποδέχθηκαν το παιχνίδι, ο διακομιστής ξεκίνησε και όλοι ήταν πρόθυμοι να παίξουν, ένας σύνδεσμος για να συνδεθείτε στο παιχνίδι εστάλη σε προσωπικά μηνύματα.

Γράψιμο matchmaking για το Dota 2014

Κάνοντας κλικ στον σύνδεσμο, ο παίκτης συνδέεται στον διακομιστή του παιχνιδιού και, στη συνέχεια, αυτό είναι. Μετά από ~25 λεπτά, το εικονικό "δωμάτιο" με παίκτες καθαρίζεται.

Ζητώ εκ των προτέρων συγγνώμη για την αμηχανία του άρθρου, δεν έχω γράψει εδώ για πολύ καιρό και υπάρχει πάρα πολύς κώδικας για να επισημάνω σημαντικές ενότητες. Χυλοπίτες, εν ολίγοις.

Αν δω ενδιαφέρον για το θέμα, θα υπάρξει ένα δεύτερο μέρος - θα περιέχει το μαρτύριο μου με πρόσθετα για srcds (Αποκλειστικός διακομιστής πηγής) και, πιθανώς, ένα σύστημα αξιολόγησης και mini-dotabuff, έναν ιστότοπο με στατιστικά παιχνιδιού.

Μερικοί σύνδεσμοι:

  1. Ο ιστότοπός μας (στατιστικά, leaderboard, μικρή σελίδα προορισμού και λήψη πελάτη)
  2. Διακομιστής Discord

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο