đź CrĂ©er un jeu de Morpion en ligne : partie 1. MĂ©canique de Jeu et Rust
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 :
- Nombre de joueurs possible entre 2 et 10.
- Taille du plateau variable.
- Nombre de coups pour gagner variable.
D’autres Ă©volutions sont envisageables :
- variante avec besoin de n fois la suite pour gagner, peut-ĂȘtre jusqu’Ă que la grille soit complĂȘte ?
- variante sans gestion de tours.
- etcâŠ
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 :
-
MorpionBoard : le plateau de jeu qui:
- garde le plateau en mémoire.
- contrÎle la validité des coups.
- calcule si le jeu est gagné.
-
Turn : Une surcouche d’un vecteur permettant d’itĂ©rer sur les identifiants de joueurs en boucle.
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 :
- un mécanisme assez souple pour gérer des chaßnes de caractÚres permettant de séparer un peu le code des jeux, du texte utilisé dans le jeu.
- la possibilitĂ© de gĂ©rer diffĂ©rentes langues. Dans mon cas, le français, l’espĂ©ranto et l’anglais.
Interface CLI
La premiĂšre version du projet est toute simple en ligne de commande3 :
Ă suivreâŠ
-
attention, je parle lĂ bien de la direction, pas du sens. ↩︎
-
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. ↩︎
-
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. ↩︎