ūüéģ 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↩︎