Skribante matchmaking por Dota 2014

Saluton

Ĉi-printempe mi renkontis projekton, en kiu la infanoj lernis kiel ruli la Dota 2-servilan version 2014 kaj, sekve, ludi sur ĝi. Mi estas granda ŝatanto de ĉi tiu ludo, kaj mi ne povis preterlasi ĉi tiun unikan ŝancon mergi min en mia infanaĝo.

Mi plonĝis tre profunde, kaj okazis, ke mi skribis Discord-roton, kiu respondecas pri preskaŭ ĉiuj funkcioj, kiuj ne estas subtenataj en la malnova versio de la ludo, nome matchmaking.
Antaŭ ĉiuj novigoj kun la bot, la vestiblo estis kreita permane. Ni kolektis 10 reagojn al mesaĝo kaj permane kunvenis servilon, aŭ gastigis lokan vestiblon.

Skribante matchmaking por Dota 2014

Mia naturo kiel programisto ne povis elteni tiom da manlaboro, kaj dum la nokto mi skizis la plej simplan version de la bot, kiu aŭtomate altigis la servilon kiam estis 10 homoj.

Mi tuj decidis skribi en nodejs, ĉar mi ne tre ŝatas Python, kaj mi sentas min pli komforta en ĉi tiu medio.

Ĉi tio estas mia unua sperto skribanta roboton por Discord, sed ĝi montriĝis tre simpla. La oficiala npm-modulo discord.js provizas oportunan interfacon por labori kun mesaĝoj, kolekti reagojn ktp.

Malgarantio: Ĉiuj kodekzemploj estas "aktualaj", tio signifas, ke ili trapasis plurajn ripetojn de reverkado nokte.

La bazo de matchmaking estas "vico" en kiu ludantoj kiuj volas ludi estas metitaj kaj forigitaj kiam ili ne volas aŭ trovas ludon.

Jen kiel aspektas la esenco de "ludanto". Komence ĝi estis nur uzantidentigilo en Discord, sed estas planoj lanĉi/serĉi ludojn de la retejo, sed unue.

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

Kaj jen la vostointerfaco. Ĉi tie, anstataŭ "ludantoj", estas uzata abstraktaĵo en formo de "grupo". Por ununura ludanto, la grupo konsistas el li mem, kaj por ludantoj en grupo, respektive, el ĉiuj ludantoj en la grupo.

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
}

Mi decidis uzi eventojn por interŝanĝi kuntekston. Ĝi estis taŭga por kazoj - dum la evento "ludo por 10 homoj estis trovita", vi povas sendi la necesan mesaĝon al la ludantoj en privataj mesaĝoj, kaj plenumi la bazan komercan logikon - lanĉi taskon por kontroli pretecon, prepari la vestiblon. por lanĉo, ktp.

Por IOC mi uzas InversifyJS. Mi havas agrablan sperton laborante kun ĉi tiu biblioteko. Rapida kaj facila!

Ni havas plurajn atendovicojn sur nia servilo - ni aldonis 1x1, normalan/taksan, kaj kelkajn kutimajn reĝimojn. Tial, ekzistas singleton RoomService kiu kuŝas inter la uzanto kaj la ludserĉo.

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

(Kodi nudelojn por doni ideon pri kiel proksimume aspektas la procezoj)

Ĉi tie mi pravigas la voston por ĉiu el la realigitaj ludreĝimoj, kaj ankaŭ aŭskultas ŝanĝojn en "grupoj" por ĝustigi la atendovicojn kaj eviti iujn konfliktojn.

Do, bone farita, mi enmetis pecojn de kodo, kiuj havas nenion komunan kun la temo, kaj nun ni iru rekte al matchmaking.

Ni konsideru la kazon:

1) La uzanto volas ludi.

2) Por komenci la serĉon, li uzas Gateway=Discord, tio estas, metas reagon al la mesaĝo:

Skribante matchmaking por Dota 2014

3) Ĉi tiu enirejo iras al RoomService kaj diras "Uzanto de malkonkordo volas eniri la atendovicon, reĝimon: netaksita ludo."

4) RoomService akceptas la peton de la enirejo kaj puŝas la uzanton (pli precize, la uzantgrupon) en la deziratan atendovicon.

5) La vico kontrolas ĉiufoje kiam estas sufiĉe da ludantoj por ludi. Se eble, elsendu eventon:

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

6) RoomService evidente feliĉe aŭskultas ĉiun vicon en maltrankvila antaŭĝojo de ĉi tiu evento. Ni ricevas liston de ludantoj kiel enigaĵon, formas virtualan "ĉambron" de ili kaj, kompreneble, eldonas eventon:

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) Do ni alvenis al la "plej alta" aŭtoritato - la klaso bot. Ĝenerale, li traktas la ligon inter enirejoj (mi ne povas kompreni kiom amuza ĝi aspektas en la rusa) kaj la komerca logiko de matchmaking. La bot aŭdas la eventon kaj ordonas al DiscordGateway sendi pretecon al ĉiuj uzantoj.

Skribante matchmaking por Dota 2014

8) Se iu malakceptas aŭ ne akceptas la ludon ene de 3 minutoj, tiam ni NE resendas ilin al la vico. Ni resendas ĉiujn aliajn al la vico kaj atendas ĝis estos denove 10 homoj. Se ĉiuj ludantoj akceptis la ludon, tiam komenciĝas la interesa parto.

Dediĉita servila agordo

Niaj ludoj estas gastigitaj en VDS kun Windows-servilo 2012. El tio ni povas tiri plurajn konkludojn:

  1. Ne estas dokisto sur ĝi, kiu trafis min en la koron
  2. Ni ŝparas sur lupago

La tasko estas ruli procezon en VDS de VPS en Linukso. Mi skribis simplan servilon en Flask. Jes, mi ne ŝatas Python, sed kion vi povas fari?Estas pli rapide kaj pli facile skribi ĉi tiun servilon sur ĝi.

Ĝi plenumas 3 funkciojn:

  1. Lanĉante servilon kun agordo - elektante mapon, la nombron da ludantoj por komenci la ludon kaj aron da kromprogramoj. Mi nun ne skribos pri kromprogramoj - tio estas alia historio kun litroj da kafo nokte miksitaj kun larmoj kaj ŝiriĝintaj haroj.
  2. Ĉesigi/rekomenci la servilon en kazo de malsukcesaj konektoj, kiujn ni povas manipuli nur permane.

Ĉio ĉi tie estas simpla, kodaj ekzemploj eĉ ne taŭgas. 100 linio skripto

Do, kiam 10 homoj kunvenis kaj akceptis la ludon, la servilo estis lanĉita kaj ĉiuj volis ludi, ligilo por konekti al la ludo estis sendita en privataj mesaĝoj.

Skribante matchmaking por Dota 2014

Alklakante la ligilon, la ludanto konektas al la ludservilo, kaj jen ĝi. Post ~25 minutoj, la virtuala "ĉambro" kun ludantoj estas malplenigita.

Mi anticipe pardonpetas pro la mallerteco de la artikolo, mi delonge ne skribis ĉi tie, kaj estas tro da kodo por reliefigi gravajn sekciojn. Nudeloj, mallonge.

Se mi vidos intereson pri la temo, estos dua parto - ĝi enhavos mian turmenton kun kromaĵoj por srcds (Fonto dediĉita servilo), kaj, verŝajne, taksadsistemo kaj mini-dotabuff, retejo kun ludstatistiko.

Kelkaj ligiloj:

  1. Nia retejo (statistiko, gvidtabulo, malgranda surterpaĝo kaj kliento elŝuto)
  2. Discord-servilo

fonto: www.habr.com

Aldoni komenton