đź 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. â©ïž