Adding a new exercise - BuggleInc/PLM GitHub Wiki
Adding an exercise to an existing lesson
Many things define an exercise in PLM. The author needs to describe the initial state of the world, informations on how to compile the student's source code, the demo code, the text explaining the objectives and the condition defining whether the exercise is passed. Each exercise must also be represented by a class extending jlm.core.model.lesson.Exercise, and an instance of this class must be declared to the lesson that contains this exercise.
Describing all of these elements manually would be a bit fastidious, so there is a lot of existing short-cuts, where PLM can infer some of the information from various source of information. For example, the wining condition is almost always defined by a comparison between the current state of the world and the objective state (this is not true for lightbot and maze exercises, where the wining condition is defined directly by the amount of light that are still on, or the amount of baggle that are still on the ground. But that's another story).
Then, the final state is never defined explicitely in the code, but instead we run the demo code on the initial state to get the final one. In turn, the initial state is most often defined programmatically directly in the Exercise class, but some universes (such as the Buggle one) allow to load a world from a text file directly.
At the end of the day, the easiest way to add a new exercise is to extend ExerciseTemplated instead of its raw Exercise ancestor. The text describing the objectives will be taken from an html file, and the demo code will be used directly to template the student code and to compute the winning condition. Here is how to do so:
Creating an exercise class
This class must extend ExerciseTemplated.
public class MyExercise extends ExerciseTemplated {
public MyExercise(Lesson lesson) {
super(lesson);
}
}
- If you want to define the initial state of the world programmatically, do so in the exercise constructor. Of course, you may want to do more things than in this little example, by adding walls and changing the ground color.
public class MyExercise extends ExerciseTemplated {
public MyExercise(Lesson lesson) {
super(lesson);
BuggleWorld myWorld = new BuggleWorld("Training World", 7,7);
new Buggle(myWorld, "Rookie", 2, 4, Direction.NORTH, Color.black, Color.lightGray);
setup(myWorld); // DON'T FORGET TO CALL THIS FUNCTION
// AT THE END OF YOUR CONSTRUCTOR
}
}
- If you want to define more than one world programmatically, that's not much more complicated:
public class MyExercise extends ExerciseTemplated {
public MyExercise(Lesson lesson) {
super(lesson);
BuggleWorld[] myWorlds = new BuggleWorld[2];
myWorlds[0] = new BuggleWorld("world 1", 7,7);
new Buggle(myWorlds[0], "buggle 1", 2, 4, Direction.NORTH, Color.black, Color.lightGray);
myWorlds[1] = new BuggleWorld("world 2", 6,6);
new Buggle(myWorlds[1], "buggle 2", 2, 4, Direction.SOUTH, Color.black, Color.lightGray);
setup(myWorlds); // DON'T FORGET TO CALL THIS FUNCTION
// AT THE END OF YOUR CONSTRUCTOR
}
}
- If you want to load a text file instead, simply use loadMap() instead of creating the world manually:
public class MyExercise extends ExerciseTemplated {
public MyExercise(Lesson lesson) {
super(lesson);
BuggleWorld myWorld = new BuggleWorld("Labyrinth", 1, 1);
loadMap(myWorld,"lessons/maze/IslandMaze");
setup(myWorld); // DON'T FORGET TO CALL THIS FUNCTION
// AT THE END OF YOUR CONSTRUCTOR
}
}
Create an entity class
This class must extend the entities of your universe (eg BuggleEntity) and be named MyExerciseEntity. If you don't use the right name, PLM won't find it and will probably complain.
This is used to provide the demo code, to compute the final objective state, and to template the student code. This last element is because most of the time, we don't want the students to write a complete class with all subtilities. Instead, we want them to write a single method that should be injected into a proper class, or even a few statements that will be injected into a proper method. Here is a typical entity class.
/* You need to put all mandatory imports there. Note that as the
student code is compiled using this as a template but in another
package, you may have to declare extra imports. */
public class MyExerciseEntity extends jlm.universe.bugglequest.SimpleBuggle {
/* You may define other methods here if you want.
This can be used to provide more tools to your students,
such as welcome.BDR defines a getIndication() to avoid the
exposition of students to Strings at this early point of
the lesson.
This can also be used to override standard methods to
remove some functionalities to the students. For example,
the maze exercises override the setPosition() method to
prevent the student from cheating this way. */
/* BEGIN TEMPLATE */
/* the code between BEGIN/END TEMPLATE will constitute the
initial version of the code presented to the student.
Conversely, the student code will be injected in place of
the template in the entity code before compilation. */
public void run() {
/* BEGIN SOLUTION */
/* the code between BEGIN/END SOLUTION will be pruned
from the code initially shown to the student */
/* END SOLUTION */
}
/* END TEMPLATE */
}
Registering your exercise in the lesson
You then want to make sure that your lesson knows about the new exercise.
This is done by calling addExercise() in the loadExercises() of your lesson class. If you want to build a chain of exercises, there is nothing more to know:
protected void loadExercises() {
addExercise(new MyFirstExercise(this));
addExercise(new MySecondExercise(this));
addExercise(new MyThirdExercise(this));
}
If you want to build a tree of exercises, where the dependencies are more finely controlled, that's not much more complicated. The following example builds a little tree of exercises.
protected void loadExercises() {
Lecture exo1 = addExercise(new Exercise1(this));
Lecture exo2 = addExercise(new Exercise2(this)); // depends on exo1 (by default)
addExercise(new Exercise1a(this),exo1); // depends on exo1
addExercise(new Exercise1b(this),exo1); // depends on exo1
addExercise(new Exercise1c(this),exo1); // depends on exo1
addExercise(new Exercise2a(this),exo2); // depends on exo2
addExercise(new Exercise2b(this),exo2); // depends on exo2
addExercise(new Exercise2c(this),exo2); // depends on exo2
}
Write the textual description
This is done by writting an HTML description file MyExercice.html. This should be done in english if you want to get it merged back into the PLM project, but there is no obligation to merge it. You are perfectly free to develop lessons for your own usage if you don't feel like contributing back to the project, in which cas you are naturally free to use the language of your choice.
Then, list this file in the po4a.conf configuration file so that it becomes translatable, and use po4a to translate it to other languages (see also PLM-Hacker-documentation).