Un bot pour Starcraft en rouille, C ou toute autre langue


StarCraft: Brood War . Ce jeu compte beaucoup pour moi! Et pour beaucoup d'entre vous, je suppose. Tellement, que je me demande si je devrais mĂȘme donner un lien vers sa page sur Wikipedia ou non.


Une fois Halt m'a envoyĂ© PM et m'a proposĂ© d'apprendre Rust . Comme toute personne ordinaire, nous avons dĂ©cidĂ© de commencer par bonjour le monde Ă©crire une bibliothĂšque dynamique pour Windows qui pourrait ĂȘtre chargĂ©e dans l'espace d'adressage de StarCraft et gĂ©rer les unitĂ©s.


L'article suivant dĂ©crira le processus de recherche de solutions et l'utilisation de technologies et de techniques qui vous permettront d'apprendre de nouvelles choses sur Rust et son Ă©cosystĂšme. Vous pouvez Ă©galement ĂȘtre inspirĂ© pour implĂ©menter un bot en utilisant votre langage prĂ©fĂ©rĂ©, que ce soit C, C ++, Ruby, Python, etc.


Il vaut vraiment la peine d'écouter l'hymne de la Corée du Sud en lisant cet article:


Starcraft OST

Bwapi


Ce jeu a presque 20 ans. Et c'est toujours populaire ; Les championnats ont attirĂ© des foules de gens aux États-Unis mĂȘme en 2017, oĂč la bataille des grands maĂźtres Jaedong contre Bisu a eu lieu. En plus des joueurs humains, les machines sans Ăąme participent Ă©galement aux batailles SC! Et cela est possible grĂące Ă  BWAPI . Liens plus utiles.


Depuis plus d'une dĂ©cennie maintenant, il y a une communautĂ© de dĂ©veloppeurs de bots autour de ce jeu. Les amateurs crĂ©ent des bots et participent Ă  divers championnats. Beaucoup d'entre eux Ă©tudient l'IA et l'apprentissage automatique. Le BWAPI est utilisĂ© par les universitĂ©s pour former leurs Ă©tudiants. Il existe mĂȘme une chaĂźne Twitch diffusant de tels matchs.


Ainsi, une équipe de fans a inversé le back-end de Starcraft il y a plusieurs années et a développé une API en C ++, qui vous permet de créer des bots, de faire des injections dans le processus de jeu et de dominer les humains misérables.


Comme cela arrive souvent, avant construire une maison, il faut extraire du minerai, forger des outils la création d'un bot, vous devez implémenter une API. Qu'est-ce que Rust a à offrir?


Ffi


Il est assez simple de travailler avec d'autres langues de Rust. Il y a un FFI pour cela. Permettez-moi de vous donner un court extrait de la documentation .


Imaginez que nous ayons une bibliothĂšque snappy , qui a un fichier d'en - tĂȘte snappy-ch , qui contient des dĂ©clarations de fonctions.


Créons un projet avec cargo .


$ cargo new --bin snappy Created binary (application) `snappy` project $ cd snappy snappy$ tree . ├── Cargo.toml └── src └── main.rs 1 directory, 2 files 

Cargo a créé une structure de fichiers standard pour le projet.


Dans Cargo.toml nous Cargo.toml dépendance à libc :


 [dependencies] libc = "0.2" 

Le fichier src/main.rs ressemblera Ă  ceci:


 extern crate libc; // To import C types, in our case for size_t use libc::size_t; #[link(name = "snappy")] // Specify the name of the library for linking the function extern { // We write the declaration of the function which we want to import // in C the declaration looks like this: // size_t snappy_max_compressed_length(size_t source_length); fn snappy_max_compressed_length(source_length: size_t) -> size_t; } fn main() { let x = unsafe { snappy_max_compressed_length(100) }; println!("max compressed length of a 100 byte buffer: {}", x); } 

Construisons et exécutons le projet:


 snappy$ cargo build ... snappy$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/snappy` max compressed length of a 100 byte buffer: 148 

Vous ne pouvez appeler que le cargo run , ce qui appelle la cargo build avant le vol. Une autre option est de construire le projet et d'appeler directement le binaire:


 snappy$ ./target/debug/snappy max compressed length of a 100 byte buffer: 148 

Si la bibliothÚque snappy est installée, le code sera compilé (pour Ubuntu, vous devez installer le paquet libsnappy-dev).


 snappy$ ldd target/debug/snappy ... libsnappy.so.1 => /usr/lib/x86_64-linux-gnu/libsnappy.so.1 (0x00007f8de07cf000) 

Comme vous pouvez le voir, notre binaire est lié à la bibliothÚque partagée libsnappy. Et un appel à snappy_max_compressed_length dans notre code est un appel de fonction de cette bibliothÚque.


rouille-bindgen


Ce serait bien si nous pouvions générer automatiquement notre FFI. Heureusement, il existe un utilitaire appelé rust-bindgen dans la boßte à outils d'un addict de Rust. Il est capable de générer des liaisons FFI vers les bibliothÚques C (et certaines C ++).


Installation:


 $ cargo install bindgen 

À quoi ressemble le rust-bindgen ? Nous prenons les fichiers d'en-tĂȘte C / C ++, nous pointons l' utilitaire bindgen vers eux, et la sortie que nous obtenons est un code Rust gĂ©nĂ©rĂ© avec les dĂ©clarations appropriĂ©es pour nous permettre d'utiliser les structures et les fonctions C. Voici ce que bindgen gĂ©nĂšre pour bindgen :


 $ bindgen /usr/include/snappy-ch | grep -C 1 snappy_max_compressed_length extern "C" { pub fn snappy_max_compressed_length(source_length: usize) -> usize; } 

Il s'avĂšre que bindgen ne peut pas faire face aux en-tĂȘtes BWAPI, gĂ©nĂ©rant des tonnes de code non utilisable (Ă  cause des fonctions membres virtuelles, std :: string dans une API publique, etc.). Le fait est que BWAPI est Ă©crit en C ++. C ++ est gĂ©nĂ©ralement difficile Ă  utiliser mĂȘme Ă  partir de projets C ++. Une fois la bibliothĂšque assemblĂ©e, il est prĂ©fĂ©rable de la lier avec le mĂȘme Ă©diteur de liens (mĂȘme version), les fichiers d'en-tĂȘte doivent ĂȘtre analysĂ©s avec le mĂȘme compilateur (mĂȘme version). Tous ces facteurs peuvent affecter le rĂ©sultat. Mangling par exemple, qui ne peut toujours pas ĂȘtre implĂ©mentĂ© sans erreurs dans GNU GCC. Ces facteurs sont si importants que mĂȘme gtest n'a pas pu le surmonter. Et dans la documentation, il est dit: vous feriez mieux de construire gtest dans le cadre du projet par le mĂȘme compilateur et le mĂȘme Ă©diteur de liens.


Bwapi-c


C est la lingua franca du génie logiciel. Si rust-bindgen fonctionne bien pour le langage C, pourquoi ne pas implémenter BWAPI pour C, puis utiliser son API? Bonne idée!


Oui, c'est une bonne idée jusqu'à ce que vous examiniez l'intérieur de BWAPI et que vous voyiez le nombre de classes et de méthodes que vous devez implémenter. Surtout toutes ces dispositions de mémoire, codes asm, correctifs de mémoire et autres "horreurs" pour lesquelles nous n'avons pas le temps. Il est nécessaire d'utiliser complÚtement la solution existante.


Mais nous devons en quelque sorte battre les fonctions de mangling, de code C ++, d'héritage et de membre virtuel.


En C ++, il existe deux outils puissants que nous utiliserons pour résoudre notre problÚme: les pointeurs opaques et le extern "C" .


extern "C" {} permet au code C ++ de se "masquer" lui-mĂȘme sous C. Il permet de gĂ©nĂ©rer des noms de fonction sans malmener.


Les pointeurs opaques nous permettent d'effacer le type et de créer un pointeur vers "un certain type" sans fournir son implémentation. Comme il ne s'agit que d'une déclaration d'un certain type, il est impossible d'utiliser ce type par valeur, vous ne pouvez l'utiliser que par pointeur.


Imaginons que nous ayons ce code C ++:


 namespace cpp { struct Foo { int bar; virtual int get_bar() { return this->bar; } }; } // namespace cpp 

Nous pouvons le transformer en un en-tĂȘte C:


 extern "C" { typedef struct Foo_ Foo; // Opaque pointer to Foo // call cpp::Foo::get_bar int Foo_get_bar(Foo* self); } 

Et voici la partie C ++ qui sera le lien entre l'en-tĂȘte C et l'implĂ©mentation C ++:


 int Foo_get_bar(Foo* self) { // cast the opaque pointer to the certain cpp::Foo and call the method ::get_bar return reinterpret_cast<cpp::Foo*>(self)->get_bar(); } 

Toutes les mĂ©thodes de cours n'ont pas dĂ» ĂȘtre traitĂ©es de cette façon. Dans BWAPI, il existe des classes que vous pouvez implĂ©menter vous-mĂȘme en utilisant les champs de ces structures, par exemple typedef struct Position { int x; int y; } Position; typedef struct Position { int x; int y; } Position; et des mĂ©thodes comme Position::get_distance .


Il y avait des cours que je devais traiter d'une maniĂšre spĂ©ciale. Par exemple, AIModule doit ĂȘtre un pointeur vers une classe C ++ avec un ensemble spĂ©cifique de fonctions membres virtuelles. NĂ©anmoins, voici l'en- tĂȘte et l' implĂ©mentation .


Ainsi, aprÚs plusieurs mois de dur labeur, 554 méthodes et une douzaine de classes, la bibliothÚque multiplateforme BWAPI-C est née, qui vous permet de créer des bots en C. Un sous-produit était la possibilité de compilation croisée et la possibilité d'implémenter l'API dans n'importe quel autre langage prenant en charge FFI et la convention d'appel cdecl.


Si vous écrivez une bibliothÚque, veuillez écrire son API en C.


La caractéristique la plus importante de BWAPI-C est l'intégration la plus large possible avec d'autres langages de programmation. Python , Ruby , Rust , PHP , Java et bien d'autres sont capables de travailler avec C, donc si vous corrigez les problÚmes et implémentez vos propres wrappers, vous pouvez également écrire un bot avec leur aide.


Écrire un bot en C


Cette partie décrit les principes généraux de l'organisation interne des modules Starcraft.


Il existe 2 types de bots: module et client. Regardons un exemple d'écriture d'un module.


Le module est une bibliothĂšque dynamique. Le principe gĂ©nĂ©ral du chargement des bibliothĂšques dynamiques peut ĂȘtre consultĂ© ici . Le module devrait exporter 2 fonctions: newAIModule et gameInit .


gameInit est facile. Cette fonction est appelée pour passer un pointeur sur une partie en cours. Ce pointeur est trÚs important, car il existe une variable statique globale dans les caractÚres génériques de BWAPI, qui est utilisée dans certaines sections du code. gameInit :


 DLLEXPORT void gameInit(void* game) { BWAPIC_setGame(game); } 

newAIModule est un peu plus compliqué. Il devrait renvoyer le pointeur vers une classe C ++, qui a une table de méthode virtuelle avec des noms comme onXXXXX qui sont appelés sur certains événements de jeu. Déclarons la structure du module:


 typedef struct ExampleAIModule { const AIModule_vtable* vtable_; const char* name; } ExampleAIModule; 

Le premier champ doit ĂȘtre un pointeur vers la table des mĂ©thodes (c'est une sorte de magie). Voici la newAIModule fonction newAIModule :


 DLLEXPORT void* newAIModule() { ExampleAIModule* const module = (ExampleAIModule*) malloc( sizeof(ExampleAIModule) ); module->name = "ExampleAIModule"; module->vtable_ = &module_vtable; return createAIModuleWrapper( (AIModule*) module ); } 

createAIModuleWrapper est une autre astuce magique qui transforme le pointeur C en pointeur vers la classe C ++ avec méthodes virtuelles fonctions des membres.


module_vtable est une variable statique sur la table des méthodes, les valeurs des méthodes sont remplies de pointeurs vers les fonctions globales:


 static AIModule_vtable module_vtable = { onStart, onEnd, onFrame, onSendText, onReceiveText, onPlayerLeft, onNukeDetect, onUnitDiscover, onUnitEvade, onUnitShow, onUnitHide, onUnitCreate, onUnitDestroy, onUnitMorph, onUnitRenegade, onSaveGame, onUnitComplete }; void onEnd(AIModule* module, bool isWinner) { } void onFrame(AIModule* module) {} void onSendText(AIModule* module, const char* text) {} void onReceiveText(AIModule* module, Player* player, const char* text) {} void onPlayerLeft(AIModule* module, Player* player) {} void onNukeDetect(AIModule* module, Position target) {} void onUnitDiscover(AIModule* module, Unit* unit) {} void onUnitEvade(AIModule* module, Unit* unit) {} void onUnitShow(AIModule* module, Unit* unit) {} void onUnitHide(AIModule* module, Unit* unit) {} void onUnitCreate(AIModule* module, Unit* unit) {} void onUnitDestroy(AIModule* module, Unit* unit) {} void onUnitMorph(AIModule* module, Unit* unit) {} void onUnitRenegade(AIModule* module, Unit* unit) {} void onSaveGame(AIModule* module, const char* gameName) {} void onUnitComplete(AIModule* module, Unit* unit) {} 

Si vous regardez le nom des fonctions et leurs signatures, il est clair dans quelles conditions et avec quels arguments elles doivent ĂȘtre appelĂ©es. Pour un exemple, j'ai rendu toutes les fonctions vides, sauf


 void onStart(AIModule* module) { ExampleAIModule* self = (ExampleAIModule*) module; Game* game = BWAPIC_getGame(); Game_sendText(game, "Hello from bwapi-c!"); Game_sendText(game, "My name is %s", self->name); } 

Cette fonction est appelée lorsque le jeu s'exécute. L'argument est un pointeur vers le module actuel. BWAPIC_getGame renvoie un pointeur global sur le jeu, que nous définissons à l'aide d'un appel à BWAPIC_setGame . Alors, montrons un exemple de travail de compilation croisée d'un module:


 bwapi-c/example$ tree . ├── BWAPIC.dll └── Dll.c 0 directories, 2 files bwapi-c/example$ i686-w64-mingw32-gcc -mabi=ms -shared -o Dll.dll Dll.c -I../include -L. -lBWAPIC bwapi-c/example$ cp Dll.dll ~/Starcraft/bwapi-data/ bwapi-c/example$ cd ~/Starcraft/bwapi-data/ Starcraft$ wine bwheadless.exe -e StarCraft.exe -l bwapi-data/BWAPI.dll --headful ... ... ... 

Appuyez sur les boutons et lancez le jeu. Plus d'informations sur la compilation et l'exĂ©cution peuvent ĂȘtre trouvĂ©es sur le site Web de BWAPI et dans BWAPI-C .


Le résultat du module:


image


Vous pouvez trouver un exemple un peu plus compliqué d'un module qui montre comment travailler avec les itérateurs, la gestion des unités, la recherche de minéraux et la sortie des statistiques dans bwapi-c / example / Dll.c.


bwapi-sys


Dans l'écosystÚme Rust, il existe une certaine façon de nommer les packages liés aux bibliothÚques natives. Tout paquet foo-sys exécute deux fonctions importantes:


  • liens avec la bibliothĂšque native libfoo;
  • fournit des dĂ©clarations aux fonctions de la bibliothĂšque libfoo. Mais des dĂ©clarations seulement! Les abstractions de haut niveau ne sont pas fournies dans les caisses * -sys.

Pour que le package * -sys puisse se lier avec succĂšs, vous devez dire Ă  cargo de rechercher la bibliothĂšque native et / ou de construire la bibliothĂšque Ă  partir des sources.


Pour que le package * -sys fournisse des dĂ©clarations, vous devez soit les Ă©crire vous-mĂȘme, soit les gĂ©nĂ©rer Ă  l'aide de bindgen. Encore une fois bindgen. Tentative numĂ©ro deux =)


La génération de fixations est super simple:


 bindgen BWAPI.h -o lib.rs \ --opaque-type ".+_" \ --blacklist-type "std.*|__.+|.+_$|Game_v(Send|Print|Draw).*|va_list|.+_t$" \ --no-layout-tests \ --no-derive-debug \ --raw-line "#![allow(improper_ctypes, non_snake_case)]" \ -- -I../submodules/bwapi-c/include sed -i -r -- 's/.+\s+(.+)_;/pub struct \1;/' lib.rs 

BWAPI.h est un fichier contenant tous les en-tĂȘtes C de BWAPI-C.


Par exemple, bindgen a déjà généré de telles déclarations pour les fonctions ci-dessus:


 extern "C" { /// BWAPIC_setGame must be called from gameInit to initialize BWAPI::BroodwarPtr pub fn BWAPIC_setGame(game: *mut Game); } extern "C" { pub fn BWAPIC_getGame() -> *mut Game; } 

Il existe deux stratégies: stocker le code généré dans le référentiel et générer le code à la volée lors de la génération. Les deux approches ont leurs avantages et leurs inconvénients .


Heureux de vous rencontrer bwapi-sys ; un petit pas de plus vers notre objectif.


Vous souvenez-vous que plus tĂŽt je parlais de multiplateforme? nlinker a rejoint le projet et mis en Ɠuvre une stratĂ©gie astucieuse. Si l'hĂŽte cible est Windows, tĂ©lĂ©chargez le BWAPIC dĂ©jĂ  assemblĂ© depuis GitHub. Et pour les cibles restantes, nous collectons BWAPI-C Ă  partir des sources d'OpenBW (je vous en parlerai un peu plus tard).


bwapi-rs


Nous avons maintenant les liaisons et nous pouvons définir des abstractions de haut niveau. Nous avons deux types avec lesquels travailler: des valeurs pures et des pointeurs opaques.


Tout est simple avec des valeurs pures. Prenons l'exemple des couleurs. Nous devons le rendre facile Ă  utiliser Ă  partir du code Rust afin d'utiliser les couleurs de maniĂšre pratique et naturelle:


 game.draw_line(CoordinateType::Screen, (10, 20), (30, 40), Color::Red); ^^^ 

Ainsi, pour une utilisation pratique, il serait nécessaire de définir l'énumération avec des constantes C ++ mais également idiomatique pour Rust, et de définir des méthodes de conversion dans bwapi_sys :: Color en utilisant std :: convert :: From:


 // FFI version #[repr(C)] #[derive(Copy, Clone)] pub struct Color { pub color: ::std::os::raw::c_int, } // Idiomatic version #[derive(PartialEq, PartialOrd, Copy, Clone)] pub enum Color { Black = 0, Brown = 19, ... 

Pour votre commodité, vous pouvez utiliser la caisse enum-primitive-derive .


Il est également facile d'utiliser des pointeurs opaques. Utilisons le modÚle Newtype :


 pub struct Player(*mut sys::Player); 

Cela signifie que Player est une sorte de structure avec un champ privé - un pointeur opaque brut de C. Et voici la façon dont vous pouvez définir la couleur Player :::


 impl Player { // so the method is declared Player::getColor in bwapi-sys //extern "C" { // pub fn Player_getColor(self_: *mut Player) -> Color; //} pub fn color(&self) -> Color { // bwapi_sys::Player_getColor - wrapper function from BWAPI-C // self.0 - opaque pointer let color = unsafe { bwapi_sys::Player_getColor(self.0) }; color.into() // cast bwapi_sys::Color -> Color } } 

Nous pouvons maintenant écrire notre premier bot en rouille!


Créer un bot dans Rust


Comme preuve de concept, le bot sera similaire à un pays bien connu: toute la tùche consiste à embaucher des travailleurs et à collecter des minéraux.


Corée du Nord


Corée du sud


Commençons par les fonctions requises gameInit et newAIModule :


 #[no_mangle] pub unsafe extern "C" fn gameInit(game: *mut void) { bwapi_sys::BWAPIC_setGame(game as *mut bwapi_sys::Game); } #[no_mangle] pub unsafe extern "C" fn newAIModule() -> *mut void { let module = ExampleAIModule { name: String::from("ExampleAIModule") }; let result = wrap_handler(Box::new(module)); result } 

#[no_mangle] remplit la mĂȘme fonction que l' extern "C" en C ++. A l'intĂ©rieur de wrap_handler toute la magie se produit, avec la substitution de la table de fonction virtuelle et de la classe C ++ "masquante".


Les définitions de la structure du module sont encore plus simples et plus sophistiquées qu'en C:


 struct ExampleAIModule { name: String, } 

Ajoutons quelques méthodes pour rendre les statistiques et donner des ordres:


 impl ExampleAIModule { fn draw_stat(&mut self) { let game = Game::get(); let message = format!("Frame {}", game.frame_count()); game.draw_text(CoordinateType::Screen, (10, 10), &message); } fn give_orders(&mut self) { let player = Game::get().self_player(); for unit in player.units() { match unit.get_type() { UnitType::Terran_SCV | UnitType::Zerg_Drone | UnitType::Protoss_Probe => { if !unit.is_idle() { continue; } if unit.is_carrying_gas() || unit.is_carrying_minerals() { unit.return_cargo(false); continue; } if let Some(mineral) = Game::get() .minerals() .min_by_key(|m| unit.distance_to(m)) { // WE REQUIRE MORE MINERALS unit.right_click(&mineral, false); } } UnitType::Terran_Command_Center => { unit.train(UnitType::Terran_SCV); } UnitType::Protoss_Nexus => { unit.train(UnitType::Protoss_Probe); } UnitType::Zerg_Hatchery | UnitType::Zerg_Lair | UnitType::Zerg_Hive => { unit.train(UnitType::Zerg_Drone); } _ => {} }; } } } 

Pour transformer le type ExampleAIModule en un véritable module, vous devez le faire répondre aux événements onXXXX . Pour ce faire, vous devez implémenter le type EventHandler, qui est un analogue de la table virtuelle AIModule_vtable de C:


 impl EventHandler for ExampleAIModule { fn on_start(&mut self) { Game::get().send_text(&format!("Hello from Rust! My name is {}", self.name)); } fn on_end(&mut self, _is_winner: bool) {} fn on_frame(&mut self) { self.draw_stat(); self.give_orders(); } fn on_send_text(&mut self, _text: &str) {} fn on_receive_text(&mut self, _player: &mut Player, _text: &str) {} fn on_player_left(&mut self, _player: &mut Player) {} fn on_nuke_detect(&mut self, _target: Position) {} fn on_unit_discover(&mut self, _unit: &mut Unit) {} fn on_unit_evade(&mut self, _unit: &mut Unit) {} fn on_unit_show(&mut self, _unit: &mut Unit) {} fn on_unit_hide(&mut self, _unit: &mut Unit) {} fn on_unit_create(&mut self, _unit: &mut Unit) {} fn on_unit_destroy(&mut self, _unit: &mut Unit) {} fn on_unit_morph(&mut self, _unit: &mut Unit) {} fn on_unit_renegade(&mut self, _unit: &mut Unit) {} fn on_save_game(&mut self, _game_name: &str) {} fn on_unit_complete(&mut self, _unit: &mut Unit) {} } 

La construction et l'exécution du module sont aussi simples que pour C:


 bwapi-rs$ cargo build --example dll --target=i686-pc-windows-gnu bwapi-rs$ cp ./target/i686-pc-windows-gnu/debug/examples/dll.dll ~/Starcraft/bwapi-data/Dll.dll bwapi-rs$ cd ~/Starcraft/bwapi-data/ Starcraft$ wine bwheadless.exe -e StarCraft.exe -l bwapi-data/BWAPI.dll --headful ... ... ... 

Et la vidéo de l'oeuvre:



Openbw


Ces gars sont allés encore plus loin. Ils ont décidé d'écrire une version open source de SC: BW! Et ils sont bons dans ce domaine. L'un de leurs objectifs était d'implémenter des images HD, mais SC: Remastered était en avance sur eux = (En ce moment, vous pouvez utiliser leur API pour écrire des bots (oui, également en C ++), mais la fonctionnalité la plus étonnante est la possibilité de visualiser rejoue directement dans votre navigateur .


Conclusion


Il y avait un problĂšme non rĂ©solu avec l'implĂ©mentation: nous ne contrĂŽlons pas que les rĂ©fĂ©rences soient uniques, donc l'existence de &mut et & vers la mĂȘme rĂ©gion entraĂźnera un comportement indĂ©fini lorsque l'objet est modifiĂ©. Une sorte de problĂšme. Halt a essayĂ© d'implĂ©menter des liaisons idiomatiques, mais il n'a pas rĂ©ussi Ă  trouver une solution. De plus, si vous souhaitez terminer cette tĂąche, vous devez soigneusement "pelleter" l'API C ++ et mettre correctement les qualificatifs const .


J'ai vraiment aimĂ© travailler sur ce projet, j'ai regardĂ© les rediffusions 하룹 ìą…ìŒ et profondĂ©ment immergĂ© dans l'atmosphĂšre. Ce jeu a fait une 놀띌욎 dent dans un univers. Aucun jeu ne peut ĂȘtre ëč„ꔐ할 수 없닀 par popularitĂ© avec SC: BW, et son impact sur ëŒ€í•œëŻŒê”­ 정ìč˜ ì—êȌ Ă©tait impensable. Les pro-gamers en CorĂ©e 아마도 sont aussi populaires que les 드띌마 ìŁŒì—° 배우 ë“€ dorams corĂ©ens diffusant aux heures de grande Ă©coute. 또한, 한ꔭ 에서 프로 êČŒìŽëšž 띌멎 ꔰ대 의 íŠč별한 ìœĄê”° 에 입대 할 수 수.


Vive StarCraft!






Un grand merci à Steve Klabnik pour m'avoir aidé à lire l'article.

Source: https://habr.com/ru/post/fr436254/


All Articles