Introduction
Lorsqu’on crée un jeu à parser, il est sans doute plus raisonnable d’utiliser un moteur existant, comme Inform, qui va gérer pour nous toute la partie technique. Autant le dire tout de suite, cet article n’est pas destiné aux personnes raisonnables, on va tenter de comprendre comment fonctionne un jeu à parser : nous allons développer une base de moteur d’analyse syntaxique simple en JavaScript, en partant de zéro.
Mise en place de la page HTML
Pour rester le plus simple possible, on va tout développer dans un seul fichier HTML, qui comprendra :
- Une div qui sera notre console de sortie
- Un champ input textuel qui attendra les commandes de l’utilisateur
<html>
<head></head>
<body>
<div style="width: 100%; height:400px;">
<div id="console" style="width: 100%; border: 1px solid;">
Bienvenue dans ce tutoriel pour apprendre les bases de la programmation d'un moteur de jeu à analyseur syntaxique simple. Vous pouvez modifier ce petit exemple de démonstration à votre convenance. AMUSEZ-VOUS BIEN !
</div>
<input style="width: 100%;" type="text" id="command" name="command" />
<div id="" style="height: 100px">
</div>
</div>
<script>
/////////////////////////////
// VOTRE CODE JAVASCRIPT ICI
</script>
</body>
</html>
Il vous suffit d’ajouter ce code dans un fichier index.html
et de double-cliquer dessus pour constater que c’est très sobre. Si vous vous sentez d’humeur créative, libre à vous de jouer avec les propriétés CSS de la page.
Le premier bout de code qu’on peut ajouter dans la section script est une petite méthode qui nous permettra d’ajouter du texte dans la console :
// DIV pour afficher le texte du jeu
let consoleDiv = document.querySelector("#console");
// Affichage d'un message dans la console du jeu
function print(message) {
let p = document.createElement("p");
p.innerHTML = message;
consoleDiv.append(p);
}
Création des données de notre jeu
Dans un premier temps, on va créer les données de notre futur jeu. L’histoire, particulièrement originale, nous met dans la peau d’un habitant d’un petit appartement comportant juste deux pièces : un salon et une cuisine. Mettons tout ça dans un tableau indexé par des clés qui nous permettront de les retrouver facilement par la suite :
// Objets et lieux du monde
let objects = {
'salon': {
'synonyms': ["SALON"],
'name': "le salon",
'location': null,
'description': "Un joli salon",
'sud': 'cuisine'
},
'cuisine': {
'synonyms': ["CUISINE"],
'name': "la cuisine",
'location': null,
'description': "Une cuisine.",
'nord': 'salon'
}
};
Pour pouvoir évoluer d’une pièce à l’autre, il nous faut des verbes de directions. Comme indiqué dans les données, la cuisine est au sud du salon. On va prévoir les verbes pour les quatre points cardinaux :
// liste des verbes et de leurs synonymes
let verbs = {
"nord": { // Nord
'synonyms':["NORD", "N", "AVAN"],
'default': "Impossible d'aller vers le nord"
},
"sud": { // Sud
'synonyms':["SUD", "S", "RECU", "ARRI"],
'default': "Impossible d'aller vers le sud"
},
"est": { // Est
'synonyms':["EST", "E", "DROI"],
'default': "Impossible d'aller vers l'est"
},
"ouest": { // Ouest
'synonyms':["OUES", "O", "GAUC"],
'default': "Impossible d'aller vers l'ouest"
}
};
Que ce soit pour les verbes ou pour les objets, on prévoit un tableau de synonymes pour que le joueur ne soit pas obligé de deviner le bon mot à chaque fois.
Pour simplifier l’analyse des verbes, on les identifiera avec les 4 premières lettres, ce qui nous évite d’avoir à gérer toutes les terminaisons possibles.
On met les synonymes en majuscule et on veut se passer des accents. On aura donc besoin d’une méthode pour retirer les accents d’une chaîne. On peut l’ajouter maintenant :
// Supprime les accents d'une chaîne
function removeAccents(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
Ajout du héros
Il nous faut un héros bien trempé pour évoluer dans l’environnement hostile de notre jeu. C’est lui qui va passer d’une pièce à l’autre au péril de sa vie.
Pour que ce soit possible, on va créer deux nouveaux objets dans notre variable objects :
let objects = {
'context':{
'synonyms': ["CONTEXT"],
'name': "contexte",
'location': null,
'description': "Tout ce qui est visible actuellement par le joueur doit être dans le contexte, directement ou indirectement"
},
'self':{
'synonyms': ["ME", "SE", "M", "SOI", "MOI"],
'name': "vous",
'location': 'context',
'description': "Mais c'est moi !"
},
// ...
}
L’objet « self » désigne le héros, on a prévu différents synonymes qui nous permettront de l’identifier dans certaines commandes.
L’objet « context » quand à lui, va constituer l’environnement direct du héros : tout ce qui est dans le contexte, directement ou indirectement, devrait pouvoir être visible ou manipulable par le joueur, à commencer par lui-même, et bien sur le lieu dans lequel il se trouve.
Initialisation du contexte
Le héros est toujours dans le contexte, c’est désigné par la ligne :
'location': 'context',
Mais on veut pouvoir gérer aussi le lieu courant dans lequel il se trouve. On va créer une variable permettant de savoir rapidement où on est et une méthode permettant de changer de lieu :
// Lieu dans lequel le joueur se trouve actuellement
let currentLocation = null;
// Changement de lieu
function changeLocation(locationIndex) {
if (currentLocation) {
objects[currentLocation].location = null;
}
currentLocation = locationIndex;
objects[currentLocation].location = 'context';
print(objects[currentLocation].description);
}
Avec ce code, on peut déjà positionner notre héros dans le salon :
changeLocation('salon');
Si tout se passe bien, la description du lieu courant devrait s’afficher dans la console à l’ouverture de la page :
Déplacements
Passons aux choses sérieuses ! Notre héros est dans son salon, mais peut-être qu’il aimerait bien aller faire un tour à la cuisine ? Il va donc falloir lire dans les pensées du joueur… Ou disons, plus simplement, interpréter les commandes qu’il va saisir.
Pour ce faire, on va déjà rattacher un événement clavier au champ de commande :
// INPUT de saisie des commandes
let commandInput = document.querySelector("#command");
commandInput.focus();
// Gestion de la validation de la commande
commandInput.addEventListener("keyup", (event) => {
if (event.key === "Enter") {
print(commandInput.value);
// TODO: faire l'analyse syntaxique de la commande saisie
commandInput.value = "";
}
window.scrollTo(0, document.body.scrollHeight);
});
En ajoutant cette méthode, à chaque fois que le joueur appuie sur la touche « entrer » dans le champ de commande, nous avons un point d’entrée pour interpréter la commande.
Pour le moment, notre code se contente de répéter le texte saisi comme un perroquet, il nous faut donc une nouvelle méthode qui sera capable de l’identifier :
function parseCommand(commandString) {
commandString = removeAccents(commandString).toUpperCase();
let verb = null;
let words = commandString.split(/[ '_.,;:]/);
if (words.length > 0) {
verb = getVerbByWord(words[0]);
}
return verb;
}
Cette méthode retourne l’index du verbe dans le liste des verbes. Pour le moment, comme on n’a que les verbes de points cardinaux, on ne prend en compte que le premier mot.
Pour que cela fonctionne, il nous faut la méthode getVerbByWord()
, qui va chercher le verbe correspondant, ainsi que sa méthode associée matchVerb
, qui vérifie que le mot saisi correspond à un des synonymes d’un verbe donné :
// Vérifie si un mot est un synonyme d'un verbe donné
function matchVerb(word, verbIndex) {
return verbs[verbIndex].synonyms.indexOf(word) > -1;
}
// Retourne la clé du verbe désigné par un mot donné
function getVerbByWord(word) {
if (!word) return null;
word = word.substr(0, 4);
for (let key in verbs) {
if (matchVerb(word, key)) {
return key;
}
}
return null;
}
A présent, dans l’event listener, si on remplace print(commandInput.value);
par :
print(parseCommand(commandInput.value));
C’est l’index du verbe qui sera affiché plutôt que la valeur saisie par l’utilisateur. Vous pouvez le vérifier en tapant juste « s » par exemple, ça devrait afficher « sud ».
Dernière étape : exécuter la commande
Maintenant que nous avons identifié le verbe, il ne nous reste plus qu’à exécuter la tâche correspondante. Ici, ça sera un déplacement dans la direction indiquée. Créons la méthode en question :
// Exécution de la commande identifiée par parseCommand()
function executeCommand(verb) {
if (verb) {
switch(verb) {
case "nord":
case "sud":
case "est":
case "ouest":
if (objects[currentLocation][verb]) {
changeLocation(objects[currentLocation][verb]);
return;
}
break;
}
print(verbs[verb].default);
return;
} else {
print("Je ne connais pas ce verbe");
return;
}
}
Et modifions l’event listener en conséquence :
// Gestion de la validation de la commande
commandInput.addEventListener("keyup", (event) => {
if (event.key === "Enter") {
executeCommand(parseCommand(commandInput.value));
commandInput.value = "";
}
window.scrollTo(0, document.body.scrollHeight);
});
Et voilà ! On peut se déplacer dans notre appartement :
Libre à vous d’ajouter autant de pièces que vous le souhaitez à votre résidence pour en faire une maison, un manoir ou un château !
Conclusion
On peut voir qu’en moins de 200 lignes de code, on parvient à créer une base de moteur de parser. La mise en œuvre n’est pas si compliquée qu’il y paraît, même si, évidemment, il faut tout de même de solides notions de programmation. Vous pouvez retrouver le code complet sur GitHub.
Si ça vous intéresse, je ferai un article plus avancé pour montrer comment on peut gérer les autres verbes. Et pour les impatients, vous pouvez examiner dès à présent une version plus avancée du programme de démonstration.
1 Ping