-
Notifications
You must be signed in to change notification settings - Fork 3
Hello Quiz
The Sphinx API is designed to be a flexible and user-friendly tool that may be useful for any project that contains quizzes of any kind. This first tutorial will cover the basics of the library by presenting the smallest code required to set up a quiz game.
In a nutshell, the API can be broken down into four main components:
- The Game system, encapsulating all the logic handling the process of the game
- The Server system, playing the role of a listener that reacts to all the different game events
- The Evaluation system, determining how accurate players' guesses are based on different implementations
- The Parsing system, allowing questions to be easily loaded from TOML files
Quiz already provides us with a default implementation of the game system, which we're going to use for this tutorial. Please check the advanced
section of the wiki for guidelines concerning implementing your own game system.
The key interface for this part is QuizGame
. It contains all the required methods that need to be implemented for the game logic. The default implementation of this interface is SimpleGame
. Let's see how to construct a game from it.
class Main {
public static void main(String... args) {
Question question = new MockQuestion();
Queue<GameRound> rounds = new LinkedList<>(Collections.singletonList(
new RaceRoundFactory().create(question)
));
Collection<Player<?>> players = Collections.singletonList(new MockPlayer("Me"));
QuizGame game = new DefaultSimpleGame<>(rounds, players);
}
}
You'll notice that two "mock" classes have been used here. This is because, for the sake of simplicity, we want to have the simplest implementations possible to keep the code short. Here are possible codes for those "mock" classes:
public class MockQuestion implements Question {
@Override
public Optional<Double> evaluateAnswer(Answer answer) {
String text = answer.getAnswerText();
double result;
switch (text) {
case "correct": result = 1; break;
case "pretty-correct": result = 0.66; break;
case "kinda-correct": result = 0.33; break;
default: result = 0;
}
return Optional.of(result);
}
@Override
public Set<QuestionTag> getTags() {
return null;
}
@Override
public void registerTag(QuestionTag tag) {
}
@Override
public void unregisterTag(QuestionTag tag) {
}
@Override
public QuestionDifficulty getDifficulty() {
return null;
}
@Override
public String getRawQuestion() {
return "How do you spell the word 'correct' ?";
}
@Override
public String getDisplayableCorrectAnswer() {
return "correct";
}
@Override
public UUID getId() {
return null;
}
}
public class MockPlayer extends Player<UUID> {
public MockPlayer() {
super(UUID.randomUUID(), "");
}
}
The reason why players are generic is that IDs may be of a custom type. For instance, the discord implementation uses discord4j
which works with so-called Snoflake
s objects as ids. In our case, we'll stick to the simple UUID
.
Now about the main
method:
First, we initialize the set of rounds (under the form of a java Queue). In our example, there is only one round: a RaceRound
. This is one of the many types of rounds that are directly provided by the API. You are free to create any kind of round you want. RaceRound
is a round where there is a single question that's displayed to everyone. The first to answer correctly gets full points, all other players get 0.
Then we initialize the collection of players. In our example, there is only one player (with a single round and a single player, it's probably not going to be the most exhilarating game ever played, but that's okay).
Eventually, we are able to construct a SimpleGame
object from those two components, with which we will be able to interact later on.
To keep this tutorial as simple as possible, we will provide a super basic implementation of the server system that displays everything happening
through System.out.println()
. A single interface encapsulates all we need for this part: GameServer
. Here is the basic implementation:
public class HelloServer implements GameServer<QuizGame> {
private boolean gameOver = false;
@Override
public void onRoundEnded(GameRoundReport report, QuizGame game) {
System.out.println("Round ended. Results:");
System.out.println(report.orderedStandings());
System.out.println();
game.nextRound();
}
@Override
public void onGameOver(List<? extends Player<?>> standings, QuizGame game) {
System.out.println("All rounds have been played. Game successfully stopped");
gameOver = true;
}
@Override
public void onPlayerGuessed(PlayerGuessContext context) {
System.out.println("Player with ID " + context.getPlayer().getId() + " sent a guess");
double pointsAwarded = context.getCorrectness();
System.out.println("Score Accuracy: " + pointsAwarded);
System.out.println("Player might try again: " + context.isEligible());
System.out.println();
}
@Override
public void onNonEligiblePlayerGuessed(Player<?> player) {
System.out.println("Input refused. Player with ID " + player.getId() + " may not send a guess");
}
@Override
public void onPlayerGaveUp(Player<?> player) {
System.out.println("Player with ID " + player.getId() + " gave up on the round");
}
@Override
public void onPlayerScoreUpdated(Player<?> player) {
System.out.println("Score update for player with ID " + player.getId() +": " + player.getScore().getPoints());
}
@Override
public void onQuestionReleased(Question question) {
System.out.println(question.getRawQuestion());
}
public boolean isGameOver() {
return gameOver;
}
}
Let's look at this code step by step.
First, you'll notice that GameServer
is a generic class. You may specify any implementation of QuizGame
you like to get more precise objects in the events.
In our case, we don't need anything more precise than QuizGame
(which is the most generic possible).
The only field of this implementation is a boolean that let us see from the outside whether the game is over or not. This will be useful later. Now one thing you need to be very careful about: servers must not contain any logic that's related to the game, but exclusively behaviors corresponding to actions that must be processed when a certain event happens in the game. All the logic must be handled by the game system.
We've talked several times already about events, but we haven't really defined them. Here is the exhaustive list:
-
onRoundEnded
is triggered when the current round is terminated. -
onGameOver
is triggered when all rounds have been played (assuming the default usage ofQuizGame
. It is possible to create a different implementation that does not use a round system, in which case this event should be triggered when the game ends). -
onPlayerGuessed
is triggered when a player sent an input to the game that corresponds to a guess. The event will only be triggered is the player was eligible, meaning that they're allowed to answer the question. -
onNonEligiblePlayerGuessed
is triggered in the opposite case, when a player tries to send a guess even though they're not allowed to, for instance if they ran out of guesses. -
onPlayerGaveUp
is triggered when a player sent an input notifying the game that they'll no longer participate to the current round. Again, this is assuming that the default round system is used, the event may be triggered differently if another implementation ofQuizGame
is provided. -
onPlayerScoreUpdated
is triggered when a player's score is updated. This event should be triggered beforeonRoundEnded
, since distributing points to players is considered to still be part of the round. -
onQuestionReleased
is triggered when a question is to be displayed to the players.
The rest of the code is relatively trivial, all instructions are for display purposes, utilizing the parameters provided as context.