diff --git a/data/quiz.txt b/data/quiz.txt index d6d1b79..6982410 100644 --- a/data/quiz.txt +++ b/data/quiz.txt @@ -55,3 +55,46 @@ Who won the 2022 World Championship?|Max Verstappen Which driver has the most career race starts?|Fernando Alonso What does VSC stand for?|Virtual Safety Car Who is the most successful British F1 driver in terms of wins?|Lewis Hamilton +What planet is closest to the sun?|Mercury +What is the longest bone in the human body?|Femur +Which ocean lies on the east coast of the USA?|Atlantic +Which organ helps you breathe?|Lungs +Who invented the telephone?|Alexander Graham Bell +What is the capital of Spain?|Madrid +What number comes after 999?|1000 +Which fruit is yellow and curved?|Banana +What is a baby dog called?|Puppy +What is the largest country in the world by area?|Russia +What is the main language spoken in China?|Mandarin +What instrument has six strings?|Guitar +What is the hottest planet in the solar system?|Venus +What natural satellite orbits Earth?|Moon +What is the capital of Australia?|Canberra +What gas do humans need to breathe?|Oxygen +How many minutes are in an hour?|60 +What do cows drink?|Water +Which continent is Egypt part of?|Africa +What is 10 × 10?|100 +Who wrote “Harry Potter”?|J.K. Rowling +What food do pandas primarily eat?|Bamboo +What is the capital of Germany?|Berlin +How many colors are in a rainbow?|7 +What is the largest bird?|Ostrich +What machine measures temperature?|Thermometer +What is the capital of Russia?|Moscow +Which animal is known for laughing?|Hyena +What planet is known for its big red spot?|Jupiter +What is the main ingredient in sushi?|Rice +Who painted The Starry Night?|Vincent van Gogh +Which country invented pizza?|Italy +What is the capital of Mexico?|Mexico City +How many letters are in the English alphabet?|26 +What is the biggest internal organ?|Liver +What animal is the symbol of Canada?|Beaver +What is the process of water turning into vapor?|Evaporation +What is the capital of South Korea?|Seoul +Which bird cannot fly?|Penguin +What is the name of Earth’s largest continent?|Asia +How many sides does a square have?|4 +Who was the first president of the United States?|George Washington +What part of the plant is typically green?|Leaf \ No newline at end of file diff --git a/src/code/ca/bcit/comp2522/lab11/Question.java b/src/code/ca/bcit/comp2522/lab11/Question.java index d671428..bd495e4 100644 --- a/src/code/ca/bcit/comp2522/lab11/Question.java +++ b/src/code/ca/bcit/comp2522/lab11/Question.java @@ -1,5 +1,19 @@ package ca.bcit.comp2522.lab11; +/** + * Represents a single quiz question containing a question prompt + * and one or more valid answers. A user-provided answer is considered + * correct if it matches any of the stored answers, ignoring case. + * + * This class performs validation to ensure question text and answer + * strings are never null or blank. + * + * @author Braeden Sowinski + * @author Nico Agostini + * @author Trishaan Shetty + * @author Calvin Arifianto + * @version 1.0.0 + */ public class Question { private static final int MIN_ANSWERS = 1; @@ -7,6 +21,12 @@ public class Question private final String question; private final String[] answers; + /** + * Validates that a string is not null or blank. + * + * @param str the string to validate + * @throws IllegalArgumentException if the string is null or blank + */ private static void validateString(final String str) { if (str == null || str.isBlank()) @@ -15,9 +35,18 @@ public class Question } } + /** + * Constructs a Question with a prompt and accepted answers. + * + * @param question the question text + * @param answers the list of valid answers + * @throws IllegalArgumentException if question is invalid, + * or if answers is empty, + * or if any individual answer is invalid + */ public Question( - final String question, - final String[] answers + final String question, + final String[] answers ) { validateString(question); @@ -35,21 +64,38 @@ public class Question this.answers = answers; } + /** + * Returns the question text. + * + * @return the question string + */ public String getQuestion() { return this.question; } + /** + * Returns the list of accepted answers. + * + * @return an array of valid answers + */ public String[] getAnswers() { return this.answers; } + /** + * Checks whether a user's answer is correct. + * Matching is case-insensitive. + * + * @param userAnswer the answer provided by the user + * @return true if it matches one of the valid answers; false otherwise + */ public boolean validateAnswer(final String userAnswer) { for (final String answer : this.answers) { - if (answer.equals(userAnswer)) + if (answer.equalsIgnoreCase(userAnswer)) { return true; } diff --git a/src/code/ca/bcit/comp2522/lab11/Quiz.java b/src/code/ca/bcit/comp2522/lab11/Quiz.java index d713749..535b49b 100644 --- a/src/code/ca/bcit/comp2522/lab11/Quiz.java +++ b/src/code/ca/bcit/comp2522/lab11/Quiz.java @@ -1,83 +1,336 @@ package ca.bcit.comp2522.lab11; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.VBox; import javafx.stage.Stage; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.Button; +import javafx.util.Duration; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Random; +/** + * A timed quiz application that loads questions from an external file + * and presents them to the user in randomized order. The user has a + * limited amount of time to answer a fixed number of questions. + * Final results are shown when time expires or all questions are answered. + * + * @author Braeden Sowinski + * @author Nico Agostini + * @author Trishaan Shetty + * @author Calvin Arifianto + * @version 1.0.0 + */ public class Quiz extends Application { - private static final int WIDTH = 800; - private static final int HEIGHT = 600; - private static final int QUESTION_SPLIT = 0; - private static final int ANSWER_SPLIT = 1; - private static final String DELIMITER = "\\|"; - private static final int NUM_QUESTIONS = 10; + private static final int WIDTH = 800; + private static final int HEIGHT = 600; + private static final int QUESTION_SPLIT = 0; + private static final int COUNTER_STARTER = 0; + private static final int ANSWER_SPLIT = 1; + private static final int ONE_SECOND = 1; + private static final int MIN_LENGTH = 2; + private static final String DELIMITER = "\\|"; + private static final int NUM_QUESTIONS = 10; + private static final int START_TIME_SECS = 90; + private static final Path QUIZ_FILE_PATH = Paths.get("comp2522", "data", "quiz.txt"); private final List questions = new ArrayList<>(); + private List quizQuestions = new ArrayList<>(); + private Question current; - private void readQuestions(final String file) + private int currentQuestionIndex; + private int correctCount; + private int wrongCount; + private int remainingSeconds; + + private Timeline timeline; + + private VBox root; + private Label questionLabel; + private Label feedbackLabel; + private Label statusLabel; + private TextField answerField; + private Button submitButton; + private Button startButton; + private Button playAgainButton; + + /** + * Reads and loads quiz questions from a file. + * + * @param path the path to the quiz data file + */ + private void readQuestions(final Path path) { - final BufferedReader reader; - - try + if (!questions.isEmpty()) { - reader = new BufferedReader(new FileReader(file)); + return; + } + try (BufferedReader reader = + new BufferedReader(new FileReader(path.toAbsolutePath().toFile()))) + { String line; while ((line = reader.readLine()) != null) { - final String question; - final String[] answers; + final String[] parts = line.split(DELIMITER); + if (parts.length < MIN_LENGTH) + { + continue; + } - question = line.split(DELIMITER)[QUESTION_SPLIT]; - answers = line.split(DELIMITER)[ANSWER_SPLIT].split(","); + final String questionText = parts[QUESTION_SPLIT]; + final String[] answers = parts[ANSWER_SPLIT].split(","); questions.add(new Question( - question, - answers + questionText, + answers )); } } catch (final FileNotFoundException e) { - System.out.println("Unable to open file: " + file); + System.out.println("Unable to open file: " + path); } catch (final IOException e) { - System.out.println("Error reading file: " + file); + System.out.println("Error reading file: " + path); } } + /** + * Initializes the UI and starts the JavaFX application. + * + * @param stage the primary stage + */ @Override public void start(final Stage stage) { - final Scene scene; - final VBox root; - root = new VBox(); root.setPrefSize(WIDTH, HEIGHT); - scene = new Scene(root); - this.current = this.questions.get(new Random().nextInt(this.questions.size())); + questionLabel = new Label("Click \"Start quiz\" to begin."); + feedbackLabel = new Label(); + statusLabel = new Label(); + answerField = new TextField(); + answerField.setPromptText("Type your answer here"); + submitButton = new Button("Submit"); + startButton = new Button("Start quiz"); + playAgainButton = new Button("Play again"); - readQuestions("./data/quiz.txt"); + answerField.setDisable(true); + submitButton.setDisable(true); + playAgainButton.setDisable(true); + playAgainButton.setVisible(false); + + root.getChildren().addAll( + questionLabel, + answerField, + submitButton, + feedbackLabel, + statusLabel, + startButton, + playAgainButton + ); + + Scene scene = new Scene(root); + + scene.getStylesheets().add( + getClass().getResource("style.css").toExternalForm() + ); + + readQuestions(QUIZ_FILE_PATH); + + if (questions.isEmpty()) + { + questionLabel.setText("No questions loaded. Check quiz.txt path/contents."); + startButton.setDisable(true); + stage.setScene(scene); + stage.show(); + return; + } + + final Runnable submitAction = () -> { + if (current == null) + { + return; + } + + final String userAnswer; + userAnswer = answerField.getText(); + + if (userAnswer == null || userAnswer.isBlank()) + { + feedbackLabel.setText("Please enter an answer."); + return; + } + + final String cleaned = userAnswer.trim(); + + if (current.validateAnswer(cleaned)) + { + correctCount++; + } + else { + wrongCount++; + } + + currentQuestionIndex++; + showNextQuestion(); + }; + + submitButton.setOnAction(event -> submitAction.run()); + answerField.setOnAction(event -> submitAction.run()); + startButton.setOnAction(event -> startGame()); + playAgainButton.setOnAction(event -> startGame()); stage.setScene(scene); + stage.setTitle("Quiz"); stage.show(); } + /** + * Starts a new quiz round, resets counters, shuffles questions, + * and initializes the timer. + */ + private void startGame() + { + correctCount = COUNTER_STARTER; + wrongCount = COUNTER_STARTER; + remainingSeconds = START_TIME_SECS; + currentQuestionIndex = COUNTER_STARTER; + + feedbackLabel.setText(""); + questionLabel.setText(""); + + quizQuestions = new ArrayList<>(questions); + Collections.shuffle(quizQuestions); + + if (quizQuestions.size() > NUM_QUESTIONS) + { + quizQuestions = new ArrayList<>(quizQuestions.subList(COUNTER_STARTER, NUM_QUESTIONS)); + } + + if (timeline != null) + { + timeline.stop(); + } + + timeline = new Timeline( + new KeyFrame(Duration.seconds(ONE_SECOND), event -> { + remainingSeconds--; + updateStatusLabel(); + + if (remainingSeconds <= COUNTER_STARTER) + { + endGame("Time's up!"); + } + }) + ); + timeline.setCycleCount(Timeline.INDEFINITE); + timeline.play(); + + answerField.setDisable(false); + submitButton.setDisable(false); + startButton.setDisable(true); + playAgainButton.setDisable(true); + playAgainButton.setVisible(false); + + showNextQuestion(); + } + + /** + * Displays the next question or ends the quiz if none remain. + */ + private void showNextQuestion() + { + if (currentQuestionIndex >= quizQuestions.size()) + { + endGame("You answered all questions."); + return; + } + + current = quizQuestions.get(currentQuestionIndex); + + questionLabel.setText(current.getQuestion()); + answerField.clear(); + feedbackLabel.setText(""); + + updateStatusLabel(); + } + + /** + * Updates the status label to show progress and remaining time. + */ + private void updateStatusLabel() + { + if (quizQuestions.isEmpty()) + { + statusLabel.setText(""); + return; + } + + final int displayIndex = + Math.min(currentQuestionIndex + ONE_SECOND, quizQuestions.size()); + + statusLabel.setText( + displayIndex + "/" + quizQuestions.size() + + " ::: " + remainingSeconds + "s remaining" + ); + } + + /** + * Ends the game, stops the timer, and displays the final score. + * + * @param reason explanation for ending (e.g., time expired) + */ + private void endGame(final String reason) + { + if (timeline != null) + { + timeline.stop(); + } + + submitButton.setDisable(true); + answerField.setDisable(true); + current = null; + + final String finalScore = + reason + "\n\nFinal score:" + + "\nCorrect: " + correctCount + + "\nWrong: " + wrongCount; + + feedbackLabel.setText(finalScore); + + questionLabel.setText("Game over."); + statusLabel.setText(""); + + playAgainButton.setVisible(true); + playAgainButton.setDisable(false); + startButton.setVisible(false); + } + + /** + * Standard Java entry point. Launches the JavaFX app. + * + * @param args CLI arguments + */ public static void main(final String[] args) { launch(args); diff --git a/src/code/ca/bcit/comp2522/lab11/style.css b/src/code/ca/bcit/comp2522/lab11/style.css new file mode 100644 index 0000000..2da606a --- /dev/null +++ b/src/code/ca/bcit/comp2522/lab11/style.css @@ -0,0 +1,63 @@ +/* Overall background */ +.root { + -fx-background-color: linear-gradient(to bottom, #fafafa, #eaeaea); +} + +/* VBox container (add style class "vbox" in code if you want this) */ +.vbox { + -fx-spacing: 10; + -fx-alignment: center; + -fx-padding: 20; + -fx-background-color: #f5f5f5; + -fx-border-color: #dddddd; + -fx-border-width: 1; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.15), 10, 0.2, 0, 2); +} + +/* Labels */ +.label { + -fx-font-size: 16px; + -fx-text-fill: #444444; + -fx-padding: 10; +} + +/* Text fields */ +.text-field { + -fx-font-size: 14px; + -fx-text-fill: #222222; + -fx-prompt-text-fill: gray; + -fx-background-color: #ffffff; + -fx-border-color: #2196F3; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-padding: 5 8; +} + +/* Optional: highlight on focus */ +.text-field:focused { + -fx-border-color: #1976D2; + -fx-effect: dropshadow(gaussian, rgba(25, 118, 210, 0.4), 8, 0.3, 0, 0); +} + +/* Buttons */ +.button { + -fx-font-size: 14px; + -fx-background-color: linear-gradient(to bottom, #4CAF50, #388E3C); + -fx-text-fill: white; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-padding: 5 14; + -fx-cursor: hand; +} + +/* Hover and pressed effects for buttons */ +.button:hover { + -fx-background-color: linear-gradient(to bottom, #66BB6A, #43A047); +} + +.button:pressed { + -fx-background-color: linear-gradient(to bottom, #388E3C, #2E7D32); + -fx-translate-y: 1; +}