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.

example dialogue zelda
Figure 1. Example of ongoing dialogue in The Legend of Zelda − Breath of the Wild

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.

example dialogue tree wikipedia
Figure 2. Example of simple dialogue tree. Each rectangle is an NPC line, and each arrow is a player choice. (source: Wikipedia)
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

2.1. Abstract syntax design

Propose a design of an abstract syntax for the Dialogue DSL, in the form of a simplified UML class diagram (ie. a metamodel).

Only very few concepts are needed for this DSL!

2.2. Concrete syntax design

Propose a design of a concrete syntax for the Dialogue DSL, using the example dialogue tree from the introduction. Underline the keywords of your concrete syntax.

3. Practice work 2: Langium grammar of the Dialogue DSL

3.1. Prerequisites

  • Make sure you have npm and node (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 .bashrc file 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.

Installing a custom node version (without being root)

If your system has node with a version before v20, then you need to install a more recent node version to be compatible with Langium.

Here is how to install node version 24 on any Linux system, without being root, using the nvm tool:

  1. If that’s not a problem for you, for a fresh start, delete your complete current npm installation and configuration:

    $ rm -rf ~/.npm ~/.npm-global ~/.npmrc
  2. Install nvm :

    $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
  3. Restart your terminal.

  4. Install node version 24 using nvm:

    $ nvm install 24
  5. Check that you have the version you want:

    $ node -v

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/src folder, 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 element keyword).

  • 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 examples folder at the root of your Langium project.

  • Deploy your DSL in a second Visual Studio Code instance.

  • In this second Visual Studio Code instance:

    • Choose "Open a folder" and select the examples folder 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.ts containing the following code:

import type { Dialogue } from 'dialogue-language';

export function generateDialogueCode(dialogue: Dialogue): string {
    return `TODO`
}
  • Modify the file packages/language/src/index.ts to export your code generator from the language package:

export * from './state-machines-code-generator.js';
  • Modify the file packages/cli/src/generator.ts of the cli package:

    • Add the following import to have access to your code generator (defined in the language package):

      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 .dialog files you prepared in the Practice Work 2.