🎼 CrĂ©er un jeu de Morpion en ligne : partie 1. MĂ©canique de Jeu et Rust

| ~ 5 mins | 1032 mots

Introduction

Dans le but d’apprendre le rust et d’autres technologies en m’amusant. J’ai dĂ©cidĂ© de commencer un projet de jeux de morpion en rust.

À l’instar des gĂ©nĂ©rateurs d’avatars, c’est un projet d’exploration qui ne va pas droit au but, mais sert de fil conducteurs Ă  divers problĂšmes et technologie que j’ai envie de tester dans des cas un minimum rĂ©els.

Les rĂšgles du jeu :

Si les jeux de base du Gomoku se fait sur un pavage de 15x15 elements et le gagnant gagne lors d’un 5 Ă  la suite, j’envisage que les briques de mon jeu puissent me permettre un peu de souplesse :

D’autres Ă©volutions sont envisageables :

Poc Rust:

Pour mon projet, mon idĂ©e est de pouvoir me baser sur des đŸ§± briques assez gĂ©nĂ©riques pour pouvoir implĂ©menter plein de variantes facilement.

J’ai ainsi rĂ©alisĂ© 2 classes de base :

J’ai aussi pas mal jouĂ© avec les traits et la gĂ©nĂ©ricitĂ© de Rust, c’est super agrĂ©able d’avoir autant de libertĂ© lĂ -dessus, reste Ă  savoir jusqu’oĂč pousser le curseur, en particulier dans un projet perso. On a vite fait de vouloir rendre tout gĂ©nĂ©rique sans cas d’usage pertinent derriĂšre.

VĂ©rifier la victoire ?

Il existe divers moyens de vĂ©rifier la victoire pour un tel jeu, mais j’ai trouvĂ© une optimisation particuliĂšrement efficace, je cherche les segments de n Ă©lĂ©ments contenant la derniĂšre position jouĂ©e, ainsi, je limite ainsi Ă©normĂ©ment l’espace parcouru :

Je les cherche donc pour chaque direction1 :

    pub(super) fn check_win(&self, position: Dimension, winning_row_length: u8) -> bool {
        self.check_win_direction(position, HORIZONTAL, winning_row_length)
            || self.check_win_direction(position, VERTICAL, winning_row_length)
            || self.check_win_direction(position, DIAG_UP, winning_row_length)
            || self.check_win_direction(position, DIAG_DOWN, winning_row_length)
    }

Et je cherche pour chaque sens de ces directions le nombre d’élĂ©ments consĂ©cutifs, auquel j’ajoute l’élĂ©ment d’origine.

fn count_consecutive_in_direction(&self, position: Dimension, direction: Direction) -> u8 {
    let res = self.count_consecutive_in_way(position, direction.0)
        + self.count_consecutive_in_way(position, direction.1)
        + 1;

Ainsi si j’obtiens pour une direction, une longueur Ă©gale ou supĂ©rieure Ă  la valeur pour gagner, je sais donc que le dernier coup a Ă©tĂ© gagnant.

Ce mécanisme est trÚs efficace.

Tentative de parallélisme

Par ailleurs, pour m’amuser2, j’ai tentĂ© d’aller encore plus loin Ă  l’aide des scoped threads de Rust. L’idĂ©e est de sĂ©parer le travail en plusieurs tĂąches rĂ©alisĂ© sĂ©parĂ©ment, si possible en parallĂšle grĂące Ă  l’existence de plusieurs cƓurs sur la machine :

    pub(super) fn check_win_multiple_core(&self, position: Dimension, winning_row_length: u8) -> bool {
        let (mut h,mut v,mut du,mut dd) = (false,false,false,false);
        thread::scope(|s| {
            s.spawn(|| {
                h = self.check_win_direction(position, HORIZONTAL, winning_row_length);
            });
            s.spawn(|| {
                v = self.check_win_direction(position, VERTICAL, winning_row_length);
            });
            s.spawn(|| {
                du = self.check_win_direction(position, DIAG_UP, winning_row_length);
            });
            s.spawn(|| {
                dd = self.check_win_direction(position, DIAG_DOWN, winning_row_length);
            });
        });
        h || v || du || dd
    }

Mais il s’avùre que cette solution qui divise le travail en 4 calculs n’est pas optimal du tout. Aprùs un test rapide avec criterion, le temps explose (ça reste trùs court cependant).

En effet, le coĂ»t de crĂ©ation des threads est beaucoup trop grand par rapport au coĂ»t du parcours de quelques Ă©lĂ©ments dans chaque direction. Pour que ça soit intĂ©ressant, il faudrait donc beaucoup plus d’élĂ©ments Ă  parcourir d’une part, mais probablement aussi de quoi pouvoir arrĂȘter les threads dĂšs que l’un d’entre a fini le travail, une sorte de « ou optimisĂ© en multicƓur Â» en quelque-sorte.

La mĂ©canique de scoped threads reste nĂ©anmoins super intĂ©ressante et facile Ă  mettre en place, il me manque juste une bonne raison de l’utiliser.

Gestion des chaĂźnes de caractĂšres

La gestion des chaĂźnes de caractĂšres en rust s’avĂšre un peu difficile provenant du python. Il est ainsi impossible d’avoir des constantes facilement pour le modĂšle du format des chaĂźnes avec la macro println! car le 1er paramĂštre est nĂ©cessairement une valeur littĂ©rale :

const mon_text: &str = "đŸŒ·";
const pattern: &str = "--{}--";
fn main() {
    println!("--{}--", "🍓"); // --🍓--
    println!("--{}--", mon_text); // --đŸŒ·--
    println!(pattern, mon_text); // error: format argument must be a string literal
}

Je ne trouve pas cela super souple. AprĂšs un peu de recherche, j’ai finalement contournĂ© pour partie le problĂšme avec une solution, plus complexe, mais plus gĂ©nĂ©rique : l’internationalisation via fluent, avec les packages i18n-embed et i18n-embed-fl ainsi un peu de code, on peut arriver Ă  faire des choses comme ça :

/i18n/fr/morpion.ftl:

game-title = Jeu de Morpion
user-turn = Tour du joueur {$name} utilisant le symbole '{$p_char}':

/i18n/eo/morpion.ftl:

game-title = Ludo de Morpion
user-turn = Vico de la ludanto {$name} kun la simbolo '{$p_char}':

/src/main.rs:

println!("{}", fl!("game-title"));
println!(
    "{}",
    fl!(
        "user-turn",
        name = players_data.names[&player_id],
        p_char = format!("{}", players_data.symbols[&player_id])
    )
);

Ce n’est pas encore parfait (je n’aime pas trop les « {} Â»), mais c’est dĂ©jĂ  plus pratique, et j’ai obtenu, d’une pierre, deux coups :

Interface CLI

La premiĂšre version du projet est toute simple en ligne de commande3 :

À suivre



  1. attention, je parle lĂ  bien de la direction, pas du sens. â†©ïžŽ

  2. Si vous ĂȘtes sur des choses un peu sĂ©rieuses, Ă©viter ce genre d’approche d’optimisation prĂ©ventive, c’est la meilleure façon de perdre du temps sans gagner en performance. Les optimisations sont un sujet sĂ©rieux qui doit ĂȘtre rĂ©flĂ©chis. â†©ïžŽ

  3. C’est une logique de dĂ©veloppement que j’ai dĂ©jĂ  utilisĂ© pour mon vieux jeu de serpent 🐍 oĂč j’ai d’abord rĂ©alisĂ© une version ligne de commande peu interactive puis une version ncurse et enfin une version pygame. Le but Ă©tant d’apprendre sĂ©parĂ©ment la mĂ©canique de jeu et la question du moteur. Si vous envisagez de faire des jeux un peu plus sĂ©rieux, je ne peux que vous conseiller d’utiliser un vrai moteur, tel que Godot↩