;;; 06-linuxplus-quiz-mode.el --- linuxplus-quiz-mode (defvar-local linuxplus-quiz-lesson-name nil) (defvar-local linuxplus-quiz-questions nil) (defvar-local linuxplus-quiz-user-answers nil) (defvar-local linuxplus-quiz-current-question 0) (defvar-local linuxplus-quiz-current-choice 0) (defvar-local linuxplus-quiz-return-window-configuration nil) (defvar-local linuxplus-quiz-return-buffer nil) (defun linuxplus-quiz--question-type (question) (plist-get question :type)) (defun linuxplus-quiz--question-prompt (question) (plist-get question :prompt)) (defun linuxplus-quiz--question-answer (question) (plist-get question :answer)) (defun linuxplus-quiz--question-answers (question) (plist-get question :answers)) (defun linuxplus-quiz--question-choices (question) (plist-get question :choices)) (defun linuxplus-quiz--current-question () (nth linuxplus-quiz-current-question linuxplus-quiz-questions)) (defun linuxplus-quiz--current-user-answer () (nth linuxplus-quiz-current-question linuxplus-quiz-user-answers)) (defun linuxplus-quiz--set-current-user-answer (answer) (setf (nth linuxplus-quiz-current-question linuxplus-quiz-user-answers) answer)) (defun linuxplus-quiz--answer-marker (answer) (cond ((eq answer 'true) "[t]") ((eq answer 'false) "[f]") ((eq answer 'yes) "[y]") ((eq answer 'no) "[n]") ((eq answer 'selected) "[*]") (t "[]"))) (defun linuxplus-quiz--answer-string (answer) (cond ((eq answer 'true) "true") ((eq answer 'false) "false") ((eq answer 'yes) "yes") ((eq answer 'no) "no") ((listp answer) (format "%S" answer)) ((null answer) "unanswered") (t (format "%S" answer)))) (defun linuxplus-quiz--correct-answer-for-display (question) (let ((type (linuxplus-quiz--question-type question))) (cond ((memq type '(true-false yes-no)) (linuxplus-quiz--answer-string (linuxplus-quiz--question-answer question))) ((eq type 'multiple) (format "%S" (linuxplus-quiz--question-answers question))) (t "unknown")))) (defun linuxplus-quiz--advance-question () (when (< linuxplus-quiz-current-question (1- (length linuxplus-quiz-questions))) (setq linuxplus-quiz-current-question (1+ linuxplus-quiz-current-question)) (setq linuxplus-quiz-current-choice 0))) (defun linuxplus-quiz--previous-question () (when (> linuxplus-quiz-current-question 0) (setq linuxplus-quiz-current-question (1- linuxplus-quiz-current-question)) (setq linuxplus-quiz-current-choice 0))) (defun linuxplus-quiz--multiple-selected-p (answer choice-index) (and (listp answer) (memq choice-index answer))) (defun linuxplus-quiz--toggle-current-choice () (let* ((answer (copy-sequence (or (linuxplus-quiz--current-user-answer) nil))) (choice linuxplus-quiz-current-choice)) (if (memq choice answer) (setq answer (delq choice answer)) (push choice answer)) (linuxplus-quiz--set-current-user-answer (sort answer #'<)))) (defun linuxplus-quiz--advance-choice () "Advance to the next multiple-choice bracket if one exists." (let* ((question (linuxplus-quiz--current-question)) (choices (linuxplus-quiz--question-choices question))) (when (and choices (< linuxplus-quiz-current-choice (1- (length choices)))) (setq linuxplus-quiz-current-choice (1+ linuxplus-quiz-current-choice))))) (defun linuxplus-quiz--answer-correct-p (question user-answer) (let ((type (linuxplus-quiz--question-type question))) (cond ((memq type '(true-false yes-no)) (eq user-answer (linuxplus-quiz--question-answer question))) ((eq type 'multiple) (equal (sort (copy-sequence (or user-answer nil)) #'<) (sort (copy-sequence (linuxplus-quiz--question-answers question)) #'<))) (t nil)))) (defun linuxplus-quiz--question-index-at-point () "Return zero-based question index at point." (save-excursion (let ((found nil)) (when (re-search-backward "^\\([0-9]+\\)\\.$" nil t) (setq found (1- (string-to-number (match-string 1))))) (when (and found (>= found 0) (< found (length linuxplus-quiz-questions))) found)))) (defun linuxplus-quiz--sync-current-position-from-point () "Update current question from cursor position." (let ((question-index (linuxplus-quiz--question-index-at-point))) (when question-index (setq linuxplus-quiz-current-question question-index) (unless (eq (linuxplus-quiz--question-type (linuxplus-quiz--current-question)) 'multiple) (setq linuxplus-quiz-current-choice 0))))) (defun linuxplus-quiz--invalid-input-message () (linuxplus-quiz--sync-current-position-from-point) (let ((type (linuxplus-quiz--question-type (linuxplus-quiz--current-question)))) (cond ((eq type 'true-false) (message "Valid input: t for true, f for false.")) ((eq type 'yes-no) (message "Valid input: y for yes, n for no.")) ((eq type 'multiple) (message "Use the space bar to add/remove the * character.")) (t (message "Invalid input."))))) (defun linuxplus-quiz--set-answer-and-maybe-advance (answer) "Set ANSWER. If unanswered, advance; if replacing, stay." (linuxplus-quiz--sync-current-position-from-point) (let ((old-answer (linuxplus-quiz--current-user-answer))) (linuxplus-quiz--set-current-user-answer answer) (unless old-answer (linuxplus-quiz--advance-question)) (linuxplus-quiz-render))) (defun linuxplus-quiz-render () (let ((inhibit-read-only t)) (erase-buffer) (insert "Linux+ Quiz\n") (insert "===========\n\n") (let ((current-type (linuxplus-quiz--question-type (linuxplus-quiz--current-question)))) (cond ((eq current-type 'true-false) (insert "Type 't' for true or 'f' for false inside the brackets.\n")) ((eq current-type 'yes-no) (insert "Type 'y' for yes or 'n' for no inside the brackets.\n")) ((eq current-type 'multiple) (insert "Use the space bar to add/remove the * character for multiple choice questions.\n")) (t (insert "Answer the question.\n")))) (insert "b = back, TAB = forward, x = finish, q = quit\n\n") (dotimes (i (length linuxplus-quiz-questions)) (let* ((question (nth i linuxplus-quiz-questions)) (type (linuxplus-quiz--question-type question)) (prompt (linuxplus-quiz--question-prompt question)) (user-answer (nth i linuxplus-quiz-user-answers)) (current-question-p (= i linuxplus-quiz-current-question))) (insert (format "%d.\n" (1+ i))) (insert (format " %s\n" prompt)) (cond ((eq type 'true-false) (insert (format "%s %s True or False\n\n" (if current-question-p ">" " ") (linuxplus-quiz--answer-marker user-answer)))) ((eq type 'yes-no) (insert (format "%s %s Yes or No\n\n" (if current-question-p ">" " ") (linuxplus-quiz--answer-marker user-answer)))) ((eq type 'multiple) (let ((choices (linuxplus-quiz--question-choices question))) (dotimes (choice-index (length choices)) (let* ((selected (if (linuxplus-quiz--multiple-selected-p user-answer choice-index) 'selected nil)) (choice-current-p (and current-question-p (= choice-index linuxplus-quiz-current-choice)))) (insert (format "%s %s %s\n" (if choice-current-p ">" " ") (linuxplus-quiz--answer-marker selected) (nth choice-index choices)))))) (insert "\n")))))) (linuxplus-quiz-goto-current-answer)) (defun linuxplus-quiz-goto-current-answer () (goto-char (point-min)) (let ((target-question (format "^%d\\.$" (1+ linuxplus-quiz-current-question)))) (when (re-search-forward target-question nil t) (let ((type (linuxplus-quiz--question-type (linuxplus-quiz--current-question)))) (cond ((memq type '(true-false yes-no)) (when (re-search-forward "\\[[^]]*\\]" nil t) (backward-char 1))) ((eq type 'multiple) (dotimes (_ linuxplus-quiz-current-choice) (re-search-forward "\\[[^]]*\\]" nil t)) (when (re-search-forward "\\[[^]]*\\]" nil t) (backward-char 1)))))))) (defun linuxplus-quiz-answer-true () (interactive) (linuxplus-quiz--sync-current-position-from-point) (if (eq (linuxplus-quiz--question-type (linuxplus-quiz--current-question)) 'true-false) (linuxplus-quiz--set-answer-and-maybe-advance 'true) (linuxplus-quiz--invalid-input-message))) (defun linuxplus-quiz-answer-false () (interactive) (linuxplus-quiz--sync-current-position-from-point) (if (eq (linuxplus-quiz--question-type (linuxplus-quiz--current-question)) 'true-false) (linuxplus-quiz--set-answer-and-maybe-advance 'false) (linuxplus-quiz--invalid-input-message))) (defun linuxplus-quiz-answer-yes () (interactive) (linuxplus-quiz--sync-current-position-from-point) (if (eq (linuxplus-quiz--question-type (linuxplus-quiz--current-question)) 'yes-no) (linuxplus-quiz--set-answer-and-maybe-advance 'yes) (linuxplus-quiz--invalid-input-message))) (defun linuxplus-quiz-answer-no () (interactive) (linuxplus-quiz--sync-current-position-from-point) (if (eq (linuxplus-quiz--question-type (linuxplus-quiz--current-question)) 'yes-no) (linuxplus-quiz--set-answer-and-maybe-advance 'no) (linuxplus-quiz--invalid-input-message))) (defun linuxplus-quiz-toggle-multiple () (interactive) (linuxplus-quiz--sync-current-position-from-point) (if (eq (linuxplus-quiz--question-type (linuxplus-quiz--current-question)) 'multiple) (progn (linuxplus-quiz--toggle-current-choice) (linuxplus-quiz--advance-choice) (linuxplus-quiz-render)) (linuxplus-quiz--invalid-input-message))) (defun linuxplus-quiz-next () (interactive) (linuxplus-quiz--sync-current-position-from-point) (let ((type (linuxplus-quiz--question-type (linuxplus-quiz--current-question)))) (if (and (eq type 'multiple) (< linuxplus-quiz-current-choice (1- (length (linuxplus-quiz--question-choices (linuxplus-quiz--current-question)))))) (setq linuxplus-quiz-current-choice (1+ linuxplus-quiz-current-choice)) (linuxplus-quiz--advance-question))) (linuxplus-quiz-render)) (defun linuxplus-quiz-prev () (interactive) (linuxplus-quiz--sync-current-position-from-point) (let ((type (linuxplus-quiz--question-type (linuxplus-quiz--current-question)))) (if (and (eq type 'multiple) (> linuxplus-quiz-current-choice 0)) (setq linuxplus-quiz-current-choice (1- linuxplus-quiz-current-choice)) (linuxplus-quiz--previous-question))) (linuxplus-quiz-render)) (defun linuxplus-quiz-invalid-key () (interactive) (linuxplus-quiz--invalid-input-message)) (defun linuxplus-quiz-return-to-lesson () "Return to the lesson/window layout from before the quiz." (interactive) (let ((config linuxplus-quiz-return-window-configuration) (buffer linuxplus-quiz-return-buffer)) (when (window-configuration-p config) (set-window-configuration config)) (when (buffer-live-p buffer) (switch-to-buffer buffer)))) (defun linuxplus-quiz-finish () (interactive) (let ((score 0) (total (length linuxplus-quiz-questions))) (dotimes (i total) (when (linuxplus-quiz--answer-correct-p (nth i linuxplus-quiz-questions) (nth i linuxplus-quiz-user-answers)) (setq score (1+ score)))) (linuxplus-record-score linuxplus-quiz-lesson-name score) (let ((inhibit-read-only t)) (erase-buffer) (insert "Linux+ Quiz Results\n") (insert "===================\n\n") (insert (format "Lesson: %s\n" linuxplus-quiz-lesson-name)) (insert (format "Score: %d/%d\n\n" score total)) (insert "Detailed review:\n\n") (dotimes (i total) (let* ((question (nth i linuxplus-quiz-questions)) (prompt (linuxplus-quiz--question-prompt question)) (user-answer (nth i linuxplus-quiz-user-answers))) (insert (format "%d. %s\n" (1+ i) prompt)) (insert (format " Your answer: %s\n" (linuxplus-quiz--answer-string user-answer))) (insert (format " Correct answer: %s\n" (linuxplus-quiz--correct-answer-for-display question))) (insert (format " Result: %s\n\n" (if (linuxplus-quiz--answer-correct-p question user-answer) "correct" "incorrect"))))) (insert "Press q to return to the lesson.\n")) (goto-char (point-min)) (linuxplus-quiz-mode) (message "Quiz complete. Score: %d/%d" score total))) (setq linuxplus-quiz-mode-map (let ((map (make-sparse-keymap))) ;; Invalid printable input by default. (dotimes (i 95) (define-key map (char-to-string (+ i 32)) #'linuxplus-quiz-invalid-key)) ;; Valid answers. (define-key map (kbd "t") #'linuxplus-quiz-answer-true) (define-key map (kbd "f") #'linuxplus-quiz-answer-false) (define-key map (kbd "y") #'linuxplus-quiz-answer-yes) (define-key map (kbd "n") #'linuxplus-quiz-answer-no) (define-key map (kbd "SPC") #'linuxplus-quiz-toggle-multiple) ;; Navigation / control. (define-key map (kbd "b") #'linuxplus-quiz-prev) (define-key map (kbd "TAB") #'linuxplus-quiz-next) (define-key map (kbd "x") #'linuxplus-quiz-finish) (define-key map (kbd "q") #'linuxplus-quiz-return-to-lesson) map)) (define-derived-mode linuxplus-quiz-mode special-mode "LinuxPlus-Quiz" "Major mode for Linux+ quizzes." (setq buffer-read-only t)) (defun linuxplus-start-quiz (lesson questions buffer) "Start a Linux+ quiz in a full window." (let ((return-config (current-window-configuration)) (return-buffer (current-buffer)) (buf (get-buffer-create buffer))) (delete-other-windows) (switch-to-buffer buf) (linuxplus-quiz-mode) (setq linuxplus-quiz-return-window-configuration return-config) (setq linuxplus-quiz-return-buffer return-buffer) (setq linuxplus-quiz-lesson-name lesson) (setq linuxplus-quiz-questions questions) (setq linuxplus-quiz-user-answers (make-list (length questions) nil)) (setq linuxplus-quiz-current-question 0) (setq linuxplus-quiz-current-choice 0) (linuxplus-quiz-render))) ;;; 06-linuxplus-quiz-mode.el ends here