Jeu de questions / réponses

Nous allons pour commencer, mettre en place un exemple simple. Nous allons écrire un logiciel de quiz qui va nous permettre de donner une liste de questions et pour chaque question nous pourrons donner des réponses erronées et une réponse correcte.

Notre programme, côté utilisateur, fonctionnera comme suit :

Quel est le langage utilisé par Ruby on Rails ?

1 - C#

2 - Python

3 - Ruby

4 - PHP

Quel est votre réponse ?

L'idée derrière ce générateur de Quiz est d'avoir un mini-langage dédié à l'écriture des questions et de ses réponses. Le programme que nous écrirons sera en mesure de lire ce mini-langage pour l'exécuter via l'interpréteur Ruby. Nous aurons donc des fichiers de quiz qui décrirons les questions / réponses :

question 'Quel est le langage utilisé par Ruby on Rails ?'

faux 'C#'

faux 'Python'

vrai 'Ruby'

faux 'PHP'

Ces questions pourront être dans un fichier 'mon_test.quiz' par exemple. Il faut comprendre que ce fichier ne sera pas réellement un fichier de données mais plutôt un programme qui sera interprété par Ruby.

Les appels à 'question', 'vrai' et 'faux' seront en fait des appels à des méthodes Ruby que nous allons définir. Nous ne parserons pas un fichier texte pour en déduire quelque chose, nous éxecuterons un vrai programme en Ruby.

Lire les questions

Nous allons commencer par implémenter la lecture de notre fichier de questions, voici ce à quoi devrait ressembler quiz.rb :

#!/usr/bin/env ruby
 
def question(text)
  puts "Voici une question: #{text}"
end
 
def vrai(text)
  puts "Voici une réponse valide: #{text}"
end
 
def faux(text)
  puts "Voici une réponse incorrecte: #{text}"
end
 
load 'mon_test.quiz'

Pas beaucoup de code me direz-vous, et bien oui mais pourtant avec si peu de code, nous avons déjà un programme fonctionnel qui va être capable de lire les questions et les réponses. Vous avez, avec ce petit morceau de code, toute la logique d'écriture d'un DSL sous les yeux. Nous avons simplement définit 3 méthodes qui vont nous permettre de lire notre fichier de quiz.

Il ne nous reste plus qu'à lire notre fichier de quiz :

load 'mon_test.quiz'

Ici encore, nous utilisons du code Ruby conventionnel qui nous sert à charger un fichier qui sera interprété comme étant du code Ruby. Notre fichier 'mon_test.quiz' n'est en fait rien d'autre qu'un fichier Ruby qui fait de multiples appels aux méthodes 'question', 'vrai' et faux' que nous avons définit avant.

Si nous lancions notre programme, nous obtiendrions :

Voici une question: Quel est le langage utilisé par Ruby on Rails ?

Voici une réponse incorrecte: C#

Voici une réponse incorrecte: Python

Voici une réponse valide: Ruby

Voici une réponse incorrecte: PHP

Création d'un quiz

Maintenant que nous sommes capable de lire les questions et réponses, il ne nous reste plus qu'à implémenter le fonctionnement de chaque méthode. Il faut donc décrire ce que feront réellement les appels à 'question', 'vrai' et faux'.

Dans notre exemple de quiz, cela reste très facile à faire puisqu'il s'agit simplement de mettre en place une structure de données. Nous allons donc créer une classe Quiz qui va nous permettre d'encapsuler le fonctionnement.

class Quiz
  def initialize
    @questions = []
  end
 
  def ajouter_question(question)
    @questions << question
  end
 
  def derniere_question
    @questions.last
  end
 
  def lancer
    count=0
    @questions.each { |q| count += 1 if q.poser }
    puts "Vous avez #{count} bonnes réponses sur #{@questions.size}."
  end
end

Cette classe va simplement nous servir à créer une collection de questions. Il nous faut maintenant une classe Question qui va nous permettre de poser une question et de savoir si la réponse donnée est bonne ou non.

class Question
 
  def initialize(text)
    @text = text
    @reponses = []
  end
 
  def ajouter_reponse(reponse)
    @reponses << reponse
  end
 
  def poser
    puts ""
    puts "Question: #{@text}"
    @reponses.size.times do |i|
      puts "#{i+1} - #{@reponses[i].text}"
    end
    print "Quel est votre réponse ? "
    reponse = gets.to_i - 1
    return @reponses[reponse].correct
  end
end

Une question représente simplement une chaîne de caractères associées à un ensemble de réponses.

Pour finir, nous aurons besoin d'une classe Reponse pour gérer les réponses utilisateur :

class Reponse
  attr_reader :text, :correct
  def initialize( text, correct )
    @text = text
    @correct = correct
  end
end

Nous n'avons plus qu'à rassembler ces éléments pour avoir un programme fonctionnel.

Et voici la touche finale qui nous permettra d'avoir un programme utilisable :

#!/usr/bin/env ruby
 
require 'quiz'
 
@quiz = Quiz.new
 
def question(text)
  @quiz.ajouter_question Question.new(text)
end
 
def vrai(text)
  @quiz.derniere_question.ajouter_reponse Reponse.new(text,true)
end
 
def faux(text)
  @quiz.derniere_question.ajouter_reponse Reponse.new(text,false)
end
 
load 'mon_test.quiz'
 
@quiz.lancer

La méthode 'question' nous permet d'ajouter un nouvel objet Question dans notre instance de Quiz. Les réponses sont collectées (pour la dernière question posée) par les méthodes 'vrai' et 'faux'. Ces deux méthodes sont identiques, seul un booléen nous permet de savoir si la réponse est correcte ou non.

Les 2 dernières lignes de notre programme nous permettent de charger les questions et de lancer le quiz. Nous aurions d'ailleurs pu mettre en place une routine qui chargerait tous les quiz présents dans un répertoire ou faire en sorte que le quiz soit un paramètre de la ligne de commande.

Si vous tester quiz.rb, vous devriez obtenir quelque chose de ce genre :

Question: Quel est le langage utilisé par Ruby on Rails ?

1 - C#

2 - Python

3 - Ruby

4 - PHP

Quel est votre réponse ? 3

Vous avez 1 bonnes réponses sur 1.

Cet exemple représente parfaitement la structure d'un DSL typique.

Nous avons commencer par définir la structure d'un quiz du plus général au plus spécifique :

  • Quiz
  • Question
  • Reponse

Nous avons ensuite définit quelques méthodes de premier niveau (accessibles directement depuis Object) qui vont nous permettre d'utiliser notre DSL (méthodes 'question', 'vrai', faux'). Après quoi nous pouvons charger les questions / réponses utilisateur (load 'mon_test.quiz') pour finalement lancer le test.

Avantages et inconvénients des DSLs internes

Les DSLs internes (le fichier de donnés est écrit dans le même langage que son interpréteur) ont des avantages certains :

  • concision (70 lignes) et clarté du code DSL
  • Possibilité d'utiliser les capacités de ruby depuis le fichier de données pour ajouter des commentaires dans le fichier de données, générer des questions posées au hasard, afficher les réponses dans un ordre aléatoire, implémenter de nouvelles fonctionnalités avancées

Voici un exemple d'une utilisation avancée de Ruby dans le fichier de données :

a = rand()
b = rand()
question "Quel est la somme de #{a} et #{b}"
faux "#{a + b + 12}"
faux "#{a + (b * 2)}"
faux "#{a + b - 0.5}"
vrai "#{a + b}"

Il y a bien évidemment des inconvénients à utiliser un DSL interne. L'avantage cité ci-dessus pourrait vite devenir un désanvatage si vous deviez limiter les possibilités côté utilisateur. L'utilisateur est ici en mesure (pour le peu qu'il connaisse Ruby) de faire tout ce que bon lui semble dans le fichier de configuration des questions / réponses. Il lui serait donc possible d'ouvrir des connexions réseau, d'écrire des fichiers, …

Voici qui clôture cette introduction sur les DSL en Ruby. Avez-vous déjà écrit des DSL ? Voyez-vous des applications concrètes et utiles de DSL ?

Bon amusement.