Software Language Engineering (SLE): implementation lab
1. Practice work introduction: "Dialogue DSL"
1.1. Dialogues in video games
In many sorts of video games, and in particular in adventure games or role-playing games, the player will be able to start a dialogue by speaking with one the characters of the game − also called the non-player characters of the game, or NPCs.
-
A dialogue begins with the NPC telling something ("Good morning, can I help you?"), then a list of possible pre-written answers are presented to the player ("I’m looking for help" or "No thanks, good bye")
-
The player can choose one of the possible answers, which will make the NPC tell something else. An important point is that what the NPC says depends on the choice just made by the player.
In most video games, the possible dialogue choices offered to the player depend on the context, such as items possessed by the player the characteristics (charisma, intelligence) of the player character, the past actions of the player, or the past choices made in the dialogue (for example one dialogue choice can make the NPC angry, and refuse to answer more questions).
| For a fun and interactive example of dialogue tree, you can visit the advertisement website of the video game Return to Monkey island, in which you can play a dialogue similar to what is found in said video game. |
1.2. Modeling dialogues as dialogue trees
In the video game industry, a typical way to encode a dialogue in a video game is in the form of a dialogue tree. The nodes of a dialogue tree represent all possible lines that a given NPC can tell the player in the dialogue. From a given node, each possible player choice is represented as an arrow going to another node of the tree.
| It is OK for a dialogue tree to contain a loop, making the dialogue possibility infinite. |
| Despite the term, a "dialogue tree" is in fact not a tree data structure, because a player choice can point to any existing node of the tree. Therefore, technically a dialog tree is more a kind of directed graph. |
1.3. Objective: creating a a Dialogue DSL
In the following series of practice works, you will have to design and implement a DSL called Dialog DSL to represent simple sorts of dialogue trees.
The Dialogue DSL should be able to represent dialog trees similar to the Figure 2 shown above.
-
A dialog tree has a name, the name of the speaking NPC, and a starting line pronounced by the NPC. It is composed of a set of NPC lines that the NPC can tell, each line being a node of the tree.
-
An NPC line contains the text pronounced by the NPC, and a list of choices that the player can make.
-
A player choice contains the text pronounced by the player, and a pointer to an existing NPC line of the tree, which will be the next line pronounced by the NPC.
We would like to implement the semantics of the Dialogue DSL as a code generator that produces Java code.
This Java code should contain a Java class with one Java method per NPC line, and a start method that calls the first NPC line.
An NPC line method should print the NPC line and print all possible choices, then should expect a keyboard input to allow the player to choose.
For example, the example dialogue tree showed before should lead to code similar to this:
public class MyDialog {
public void start() {
line1();
}
public void line1() {
Util.display("You don't look like you're from around here");
Util.display("(1) I've lived here all my life!");
Util.display("(2) I came here from Newton.");
int choice = Util.askUserChoice(2);
switch (choice) {
case 1:
line2();
break;
case 2:
line3();
break;
}
}
public void line2() {
Util.display("Oh really? Then you must know Mr. Bowler.");
Util.display("(1) Mr. Bowler is a good friend of mine!");
Util.display("(2) Who?");
int choice = Util.askUserChoice(2);
switch (choice) {
case 1:
line4();
break;
case 2:
line5();
break;
}
}
public void line3() {
Util.display("Newton, eh? I heard there's trouble brewing down there.");
Util.display("(1) I haven't heard about any trouble.");
Util.display("(2) Did I say Newton? I'm actually from Springville.");
int choice = Util.askUserChoice(2);
switch (choice) {
case 1:
line5();
break;
case 2:
line2();
break;
}
}
public void line4() {
Util.display("You liar! There ain't no Mr. Bowler, I made him up!");
}
public void line5() {
Util.display("Don't you worry about it. Say, do you have something to eat? I'm starving.");
}
}
This assumes that an Util class is already available with the following methods:
import java.util.Scanner;
public class Util {
static Scanner scanner = new Scanner(System.in);
public static int askUserChoice(int nbChoices) {
int choice = -1;
while (choice < 0 || choice >= nbChoices) {
System.out.println("\nEnter choice (1-" + nbChoices + "): ");
String choiceString = scanner.nextLine();
try {
choice = Integer.parseInt(choiceString);
} catch (NumberFormatException e) {
System.out.println("Input must be a number.");
}
}
return choice;
}
public static void display(String s) {
System.out.println(s);
}
}
2. Practice work 1: design of the Dialogue DSL
3. Practice work 2: Langium grammar of the Dialogue DSL
3.1. Prerequisites
-
Make sure you have
npmandnode(at least version 20) available in your system. -
Make sure you have configured npm to allow installation of packages globally:
-
Run this command:
npm config set prefix ~/.local -
Modify your
.bashrcfile and add:export PATH=~/.local/bin:$PATH -
Restart your terminal
-
-
Install Yeoman and the Langium project generator:
npm i -g yo generator-langium -
Install the official Langium extension for Visual Studio Code.
3.2. Langium project generation
Using Yeoman, initialize a Langium project for your Dialogue DSL with the following choices:
-
Your extension name:
dialogue-dsl -
Your language name:
Dialogue DSL -
File extensions:
.dialog -
Your grammar entry rule name: here put the name of the root concept of your abstract syntax design
-
Include VSCode extension?: Yes
-
Generate example project?: No
-
Include CLI?: Yes
-
Include language tests?: No
Before going further, have a look at the default language initialized by Langium at the project generator.
Its placeholder concrete syntax can only be used to declare elements (with the keyword element).
-
In the
packages/language/srcfolder, have a look at the Langium grammar (.langium) of the language.Contrary to the method we saw in the course, this default Langium grammar does not use declared types to properly separate the abstract syntax from the concrete syntax. We will do things differently (and better) for the Dialogue DSL! -
Call the Langium generator on this default project, and build the project.
-
Test the language and see how the generated editor works.
3.3. Abstract syntax in Langium
In the packages/language/src folder, implement your abstract syntax in a new Langium grammar file called dialogue-dsl-abstractsyntax.langium.
Do not write your abstract syntax in the existing dialogue-dsl.langium.
|
3.4. Concrete syntax in Langium
Implement your concrete syntax in the dialogue-dsl.langium Langium grammar file.
You will have to:
-
Keep default the terminal grammar rules untouched.
-
Remove the generated example parser grammar rules (with the
elementkeyword). -
Write your own parser grammar rules based on your concrete syntax design.
| Each concept of your abstract syntax should have a parser rule able to instantiate this concept. |
3.5. Testing your language
-
Call the Langium generator and build your project.
-
Create a new
examplesfolder at the root of your Langium project. -
In this second Visual Studio Code instance:
-
Choose "Open a folder" and select the
examplesfolder created in the above step. -
Create a new file
example.dialog, and use your concrete syntax to create an example dialogue tree similar to the Figure 2 shown above. Your code should be correctly parsed by the editor, with working syntax highlighting, auto-completion, and syntax error highligting.
-
4. Practice work 3: Code generator for the Dialogue DSL
Using Langium, implement the expected code generator
-
Create a new file
packages/language/src/dialogue-code-generator.tscontaining the following code:
import type { Dialogue } from 'dialogue-language';
export function generateDialogueCode(dialogue: Dialogue): string {
return `TODO`
}
-
Modify the file
packages/language/src/index.tsto export your code generator from thelanguagepackage:
export * from './state-machines-code-generator.js';
-
Modify the file
packages/cli/src/generator.tsof theclipackage:-
Add the following import to have access to your code generator (defined in the
languagepackage):import { generateDialogueCode } from 'dialogue-language'; -
Replace the following code:
const fileNode = expandToNode` // TODO : place here generated code `.appendNewLineIfNotEmpty();by
const fileNode = generateDialogCode(model)to call your code generator.
-
-
Now you need to do the actual work of implementing the expected code generator in
packages/language/src/dialogue-code-generator.ts(as specified with an example in section Section 1.3 of this page). -
After building your project, test your code generator on your own dialogue trees using the command
node ./packages/cli/bin/cli.js generate. You can use example.dialogfiles you prepared in the Practice Work 2.