Migrate to Gitea
This commit is contained in:
3
src/course/mod.rs
Normal file
3
src/course/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub(crate) mod units;
|
||||
pub(crate) mod story;
|
||||
pub(crate) mod tracker;
|
218
src/course/story.rs
Normal file
218
src/course/story.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{files, learner::profile, printers::{self, clear_console, print_boxed, print_screen}, utilities::questions::{ask_mcq, ask_multi_select, ask_plaintext}, Module};
|
||||
use super::units;
|
||||
|
||||
const REQUIRED_FOR_MASTERY: f64 = 0.1;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct UnitStory {
|
||||
unit: Module,
|
||||
title: String,
|
||||
screens: Vec<Screen>
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum ScreenType {
|
||||
Text,
|
||||
Mcq,
|
||||
Checkboxes,
|
||||
FreeResponse,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Screen {
|
||||
screen_type: ScreenType,
|
||||
text: String,
|
||||
question: Option<String>,
|
||||
options: Option<Vec<String>>,
|
||||
correct_indices: Option<Vec<usize>>,
|
||||
correct_text: Option<Vec<String>>,
|
||||
hints: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub fn outer_loop(learner: &mut profile::Learner) {
|
||||
loop {
|
||||
let next_unit = select_next_unit(&learner);
|
||||
if next_unit.is_none() {
|
||||
print_boxed("Congratulations! You have completed all the modules. You are now a master of the galaxy! I have nothing new to teach you. Well done.", printers::StatementType::CorrectFeedback);
|
||||
println!("Saving your progress...");
|
||||
files::save_to_file(&learner.filename, &learner)
|
||||
.expect("Failed to save profile file");
|
||||
println!("Progress saved. Goodbye!");
|
||||
return;
|
||||
}
|
||||
let next_unit = next_unit.unwrap();
|
||||
inner_loop(next_unit.clone(), learner);
|
||||
if learner.progress.get(&next_unit).map_or(true, |p| p.get_probability() >= 1.0) {
|
||||
print_boxed(&format!("You have completed {:?}", next_unit), printers::StatementType::GeneralFeedback);
|
||||
} else {
|
||||
print_boxed(&format!("I think we still need to practice {:?}. Let's keep practicing before we move on.", next_unit), printers::StatementType::GeneralFeedback);
|
||||
}
|
||||
let answer = ask_mcq("Do you want to keep going or leave and save?", &[
|
||||
"Keep working (and save the galaxy)",
|
||||
"Leave and save (risking all life in the galaxy)",
|
||||
]).unwrap();
|
||||
if answer == "Keep working (and save the galaxy)" {
|
||||
print_boxed(&format!("Great! Let's carry on."), printers::StatementType::GeneralFeedback);
|
||||
println!("Saving your progress...");
|
||||
files::save_to_file(&learner.filename, &learner)
|
||||
.expect("Failed to save profile file");
|
||||
println!("Progress saved. Let's keep going!");
|
||||
learner.display_progress();
|
||||
continue;
|
||||
} else {
|
||||
println!("Bummer. We were really counting on you, but I guess you probably have human needs to attend to.");
|
||||
println!("Saving your progress...");
|
||||
files::save_to_file(&learner.filename, &learner)
|
||||
.expect("Failed to save profile file");
|
||||
println!("Progress saved. Goodbye!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner_loop(unit: Module, learner: &mut profile::Learner) {
|
||||
print_boxed(&format!("The unit is {}", unit), printers::StatementType::GeneralFeedback);
|
||||
let filename = unit.get_filename();
|
||||
let unit_story = files::parse_unit(&filename).unwrap();
|
||||
let mut milestones = 0;
|
||||
printers::unit_screen(&unit_story.title);
|
||||
for screen in &unit_story.screens {
|
||||
match screen.screen_type {
|
||||
ScreenType::Text => { }
|
||||
_ => {
|
||||
milestones += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(progress) = learner.progress.get_mut(&unit) {
|
||||
progress.set_milestones(milestones);
|
||||
progress.set_completed_milestones(0);
|
||||
}
|
||||
for screen in unit_story.screens {
|
||||
let mut attempts = 0;
|
||||
match screen.screen_type {
|
||||
ScreenType::Text => {
|
||||
clear_console();
|
||||
print_screen(&screen.text);
|
||||
}
|
||||
ScreenType::Mcq => {
|
||||
let options: Vec<&str> = screen.options.as_ref().unwrap().iter().map(|s| s.as_str()).collect();
|
||||
let mut options = screen.options.clone().unwrap();
|
||||
options.push("Get hint".to_string());
|
||||
let options: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
|
||||
loop {
|
||||
let answer = ask_mcq(&screen.question.clone().unwrap(), &options).unwrap();
|
||||
let correct = if screen.correct_indices.as_ref().unwrap().contains(&options.iter().position(|x| *x == answer).unwrap_or(usize::MAX)) {
|
||||
print_boxed("Correct!", printers::StatementType::CorrectFeedback);
|
||||
true
|
||||
} else {
|
||||
if screen.hints.is_some() {
|
||||
if attempts == screen.hints.iter().len() {
|
||||
let correct_indices_text: Vec<_> = screen.correct_indices.unwrap().iter().map(|&i| options[i]).collect();
|
||||
print_boxed(&format!("It seems like these hints aren't working. The answer is: {:?}", correct_indices_text), printers::StatementType::IncorrectFeedback);
|
||||
break;
|
||||
} else {
|
||||
let hint = screen.hints.as_ref().unwrap()[attempts].clone();
|
||||
print_boxed(&format!("Please try the question again. Here's a hint: {}", hint), printers::StatementType::IncorrectFeedback);
|
||||
println!("Please try the question again. Here's a hint: {}", hint);
|
||||
attempts += 1;
|
||||
}
|
||||
}
|
||||
false
|
||||
};
|
||||
learner.update_progress(unit.clone(), correct);
|
||||
learner.progress.get_mut(&unit).unwrap().print_progress();
|
||||
if correct {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ScreenType::Checkboxes => {
|
||||
let options: Vec<&str> = screen.options.as_ref().unwrap().iter().map(|s| s.as_str()).collect();
|
||||
let mut options = screen.options.clone().unwrap();
|
||||
options.push("Get hint".to_string());
|
||||
let options: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
|
||||
loop {
|
||||
let answers = ask_multi_select(&screen.question.clone().unwrap(), &options).unwrap();
|
||||
let correct_indices: Vec<usize> = screen.correct_indices.as_ref().unwrap().to_vec();
|
||||
let selected_indices: Vec<usize> = answers.iter().filter_map(|answer| options.iter().position(|x| *x == *answer)).collect();
|
||||
let correct = selected_indices.iter().all(|&index| correct_indices.contains(&index)) && selected_indices.len() == correct_indices.len();
|
||||
if correct {
|
||||
print_boxed("Correct!", printers::StatementType::CorrectFeedback);
|
||||
learner.update_progress(unit.clone(), true);
|
||||
learner.progress.get_mut(&unit).unwrap().print_progress();
|
||||
break;
|
||||
} else {
|
||||
if screen.hints.is_some() {
|
||||
if attempts == screen.hints.iter().len() {
|
||||
print_boxed(&format!("It seems like these hints aren't working. The correct answers are: {:?}", correct_indices.iter().map(|&i| options[i]).collect::<Vec<_>>()), printers::StatementType::IncorrectFeedback);
|
||||
break;
|
||||
} else {
|
||||
let hint = screen.hints.as_ref().unwrap()[attempts].clone();
|
||||
let hint = format!("{}\nThere are {} correct answers.", hint, correct_indices.len());
|
||||
print_boxed(&format!("Please try the question again. Here's a hint: {}", hint), printers::StatementType::IncorrectFeedback);
|
||||
println!("Please try the question again. Here's a hint: {}", hint);
|
||||
attempts += 1;
|
||||
}
|
||||
}
|
||||
learner.update_progress(unit.clone(), false);
|
||||
learner.progress.get_mut(&unit).unwrap().print_progress();
|
||||
}
|
||||
}
|
||||
}
|
||||
ScreenType::FreeResponse => {
|
||||
loop {
|
||||
let answer= ask_plaintext(&screen.question.clone().unwrap()).unwrap();
|
||||
let correct_text: Vec<String> = screen.correct_text.as_ref().unwrap().to_vec();
|
||||
let normalized_answer = answer.trim().to_lowercase().replace(|c: char| !c.is_alphanumeric(), "");
|
||||
let correct = screen.correct_text.as_ref().unwrap().iter().any(|i| {
|
||||
let normalized_correct_answer = i.trim().to_lowercase().replace(|c: char| !c.is_alphanumeric(), "");
|
||||
normalized_answer == normalized_correct_answer
|
||||
});
|
||||
if correct {
|
||||
print_boxed("Correct!", printers::StatementType::CorrectFeedback);
|
||||
learner.update_progress(unit.clone(), true);
|
||||
learner.progress.get_mut(&unit).unwrap().print_progress();
|
||||
break;
|
||||
} else {
|
||||
if screen.hints.is_some() {
|
||||
if attempts == screen.hints.iter().len() {
|
||||
print_boxed(&format!("It seems like these hints aren't working. The correct answers are: {:?}", correct_text), printers::StatementType::IncorrectFeedback);
|
||||
break;
|
||||
} else {
|
||||
let hint = screen.hints.as_ref().unwrap()[attempts].clone();
|
||||
print_boxed(&format!("Please try the question again. Here's a hint: {}", hint), printers::StatementType::IncorrectFeedback);
|
||||
println!("Please try the question again. Here's a hint: {}", hint);
|
||||
attempts += 1;
|
||||
}
|
||||
}
|
||||
learner.update_progress(unit.clone(), false);
|
||||
learner.progress.get_mut(&unit).unwrap().print_progress();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next_unit(learner: &profile::Learner) -> Option<units::Module> {
|
||||
let mut modules = units::Module::iter();
|
||||
if learner.progress.get(&Module::Introduction).map_or(true, |p| p.get_probability() == 0.0) {
|
||||
return Some(crate::Module::Introduction);
|
||||
}
|
||||
for module in modules {
|
||||
if let Some(progress) = learner.progress.get(&module) {
|
||||
if progress.get_probability() < REQUIRED_FOR_MASTERY {
|
||||
if progress.get_probability() != 0.0 {
|
||||
print_boxed(&format!("It looks like you haven't mastered {} yet. Let's work on that.", module), printers::StatementType::GeneralFeedback);
|
||||
}
|
||||
return Some(module);
|
||||
}
|
||||
// if progress.get_milestones_completed() == 0 {
|
||||
// return Some(module);
|
||||
// }
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
141
src/course/tracker.rs
Normal file
141
src/course/tracker.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use crossterm::{style::Stylize, terminal::size};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::units::Module;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub(crate) struct Tracker {
|
||||
pub(crate) unit: Module,
|
||||
questions_answered: u64,
|
||||
questions_correct: u64,
|
||||
probability_known: f64,
|
||||
results: Vec<bool>, // Vector to store correct/incorrect results
|
||||
milestones: usize,
|
||||
milestones_completed: usize,
|
||||
}
|
||||
|
||||
impl Tracker {
|
||||
pub(crate) fn new(unit: Module) -> Self {
|
||||
Self {
|
||||
unit,
|
||||
questions_answered: 0,
|
||||
questions_correct: 0,
|
||||
probability_known: 0.3, // Start with a 30% chance of knowing the answer
|
||||
results: Vec::new(),
|
||||
milestones: 0,
|
||||
milestones_completed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn quiz_result(&mut self, correct: bool) -> f64 {
|
||||
self.questions_answered += 1;
|
||||
self.results.push(correct);
|
||||
if correct {
|
||||
self.milestones_completed = (self.milestones_completed + 1).min(self.milestones);
|
||||
self.questions_correct += 1;
|
||||
}
|
||||
|
||||
// Calculate streak of correct answers
|
||||
let mut streak = 0;
|
||||
for &result in self.results.iter().rev() {
|
||||
if result {
|
||||
streak += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let prior = self.probability_known;
|
||||
let slip = if self.questions_answered < 10 {
|
||||
0.05 // Lower slip rate for beginners
|
||||
} else if self.probability_known > 0.8 {
|
||||
0.15 // Higher slip rate for advanced students
|
||||
} else {
|
||||
0.1 // Default slip rate
|
||||
};
|
||||
let guess = if self.questions_answered < 10 {
|
||||
0.2 // Lower guess rate for beginners
|
||||
} else if self.probability_known > 0.8 {
|
||||
0.3 // Higher guess rate for advanced students
|
||||
} else {
|
||||
0.25 // Default guess rate (average of 4 options)
|
||||
};
|
||||
let likelihood = if correct {
|
||||
1.0 - slip
|
||||
} else {
|
||||
guess
|
||||
};
|
||||
|
||||
// Adjust posterior based on streak
|
||||
let streak_multiplier = 1.0 + (streak as f64 * 0.1); // Increase probability for streaks
|
||||
let posterior = if prior == 0.0 {
|
||||
likelihood * streak_multiplier // Start with likelihood if prior is zero
|
||||
} else {
|
||||
((likelihood * prior) / ((likelihood * prior) + ((1.0 - likelihood) * (1.0 - prior)))) * streak_multiplier
|
||||
};
|
||||
|
||||
self.probability_known = posterior.min(1.0).max(0.0);
|
||||
|
||||
self.probability_known
|
||||
}
|
||||
|
||||
pub(crate) fn get_probability(&self) -> f64 {
|
||||
// self.probability_known
|
||||
if self.milestones == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
if self.milestones_completed == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.milestones_completed as f64 / self.milestones as f64
|
||||
}
|
||||
|
||||
pub fn get_milestones(&self) -> usize {
|
||||
self.milestones
|
||||
}
|
||||
pub fn get_milestones_completed(&self) -> usize {
|
||||
self.milestones_completed
|
||||
}
|
||||
|
||||
pub(crate) fn print_progress(&self) {
|
||||
let (cols, _) = size().unwrap();
|
||||
// let percent = self.probability_known * 100.0; // Assuming `probability_known` is a method in `LessonTracker`
|
||||
let percent = if self.milestones_completed > 0 {
|
||||
(self.milestones_completed as f64) / (self.milestones as f64) * 100.0 // Assuming `probability_known` is a method in `LessonTracker`
|
||||
} else {0.0};
|
||||
let percent_name = format!("{:>4.0}%", percent).dark_blue();
|
||||
let unit_name = format!("{} {} ", percent_name, self.unit);
|
||||
let dots = ".".repeat((cols as usize).saturating_sub(unit_name.len() + 11));
|
||||
match percent {
|
||||
percent if percent > 0.0 => {
|
||||
let progress = percent as usize;
|
||||
let filled_percent = progress / 5; // Each '=' represents 5%
|
||||
let bars = format!(
|
||||
"|{}{}|",
|
||||
"█".repeat(filled_percent),
|
||||
" ".repeat(20_usize.saturating_sub(filled_percent)),
|
||||
);
|
||||
println!(
|
||||
"{} {} {}",
|
||||
unit_name,
|
||||
dots,
|
||||
bars,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
"{} {} |{}|",
|
||||
unit_name,
|
||||
dots,
|
||||
" ".repeat(20),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn set_milestones(&mut self, milestones: usize) {
|
||||
self.milestones = milestones;
|
||||
}
|
||||
pub fn set_completed_milestones(&mut self, completed_milestones: usize) {
|
||||
self.milestones_completed = completed_milestones;
|
||||
}
|
||||
}
|
382
src/course/units.rs
Normal file
382
src/course/units.rs
Normal file
@@ -0,0 +1,382 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use rand::{rng, seq::IndexedRandom};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
// use strum_macros::EnumIter;
|
||||
|
||||
/// Module defines the structure and data for the course units, including lessons and modules.
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)]
|
||||
pub(crate) enum Module {
|
||||
Introduction,
|
||||
Logic(Logic),
|
||||
Fallacy(Fallacy),
|
||||
Bias(Bias),
|
||||
Appraisal(Appraisal),
|
||||
}
|
||||
impl Default for Module {
|
||||
fn default() -> Self {
|
||||
Module::Introduction
|
||||
}
|
||||
}
|
||||
impl fmt::Display for Module {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Module::Introduction => write!(f, "Introduction"),
|
||||
Module::Logic(logic) => write!(f, "Logic: {}", logic),
|
||||
Module::Fallacy(fallacy) => write!(f, "Fallacy: {}", fallacy),
|
||||
Module::Bias(bias) => write!(f, "Bias: {}", bias),
|
||||
Module::Appraisal(appraisal) => write!(f, "Appraisal: {}", appraisal),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Module {
|
||||
pub(crate) fn get_filename(&self) -> String {
|
||||
match self {
|
||||
Module::Introduction => format!("data/introduction.json"),
|
||||
Module::Logic(logic) => format!("data/logic/{}.json", logic.get_filename()),
|
||||
Module::Fallacy(fallacy) => format!("data/fallacy/{}.json", fallacy.get_filename()),
|
||||
Module::Bias(bias) => format!("data/bias/{}.json", bias.get_filename()),
|
||||
Module::Appraisal(appraisal) => format!("data/appraisal/{}.json", appraisal.get_filename()),
|
||||
}
|
||||
}
|
||||
pub(crate) fn random() -> Self {
|
||||
let mut rng = rng();
|
||||
let variants = [
|
||||
Module::Logic(Logic::random()),
|
||||
Module::Fallacy(Fallacy::random()),
|
||||
Module::Bias(Bias::random()),
|
||||
Module::Appraisal(Appraisal::random()),
|
||||
];
|
||||
variants.choose(&mut rng).unwrap().clone()
|
||||
}
|
||||
pub fn iter() -> impl Iterator<Item = Self> {
|
||||
vec![
|
||||
Module::Introduction,
|
||||
Module::Logic(Logic::LogicalOperations),
|
||||
Module::Logic(Logic::BooleanAlgebra),
|
||||
Module::Fallacy(Fallacy::FormalFallacy(FormalFallacy::PropositionalFallacy)),
|
||||
Module::Fallacy(Fallacy::FormalFallacy(FormalFallacy::ProbabilisticFallacy)),
|
||||
Module::Fallacy(Fallacy::FormalFallacy(FormalFallacy::SyllogisticFallacy)),
|
||||
Module::Fallacy(Fallacy::FormalFallacy(FormalFallacy::QuantificationalFallacy)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::PostHocErgoPropterHoc)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::SlipperySlope)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::TexasSharpshooter)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::HastyGeneralization)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::OverGeneralization)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::NoTrueScotsman)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::QuotingOutOfContext)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::AdHominem)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::TuQuoque)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::Bandwagon)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::StrawMan)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::AdIgnorantiam)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::SpecialPleading)),
|
||||
Module::Fallacy(Fallacy::InformalFallacy(InformalFallacy::BeggingTheQuestion)),
|
||||
Module::Bias(Bias::ConfirmationBias),
|
||||
Module::Bias(Bias::TheHaloEffect),
|
||||
Module::Bias(Bias::FundamentalAttributionError),
|
||||
Module::Bias(Bias::InGroupBias),
|
||||
Module::Bias(Bias::DunningKrugerEffect),
|
||||
Module::Bias(Bias::BarnumEffect),
|
||||
Module::Appraisal(Appraisal::ConversionToPropositional),
|
||||
Module::Appraisal(Appraisal::CounterArgument),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
// pub(crate) fn create_empty_files() -> std::io::Result<()> {
|
||||
|
||||
// for module in Module::iter() {
|
||||
// let filename = module.get_filename();
|
||||
// let path = Path::new(&filename);
|
||||
// if let Some(parent) = path.parent() {
|
||||
// fs::create_dir_all(parent)?;
|
||||
// }
|
||||
// fs::File::create(path)?;
|
||||
// }
|
||||
// Ok(())
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
/// Logic module, which includes various lessons related to logical reasoning and operations.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum Logic {
|
||||
LogicalOperations,
|
||||
BooleanAlgebra,
|
||||
}
|
||||
impl Default for Logic {
|
||||
fn default() -> Self {
|
||||
Logic::LogicalOperations
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Logic {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Logic::LogicalOperations => write!(f, "Logical Operations"),
|
||||
Logic::BooleanAlgebra => write!(f, "Boolean Algebra"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Logic {
|
||||
pub(crate) fn get_filename(&self) -> String {
|
||||
match self {
|
||||
Logic::LogicalOperations => "logical_operations".to_string(),
|
||||
Logic::BooleanAlgebra => "boolean_algebra".to_string(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn random() -> Self {
|
||||
let mut rng = rng();
|
||||
let variants = [Logic::LogicalOperations, Logic::BooleanAlgebra];
|
||||
variants.choose(&mut rng).unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallacy module, which includes various lessons related to logical fallacies and errors in reasoning.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum Fallacy {
|
||||
FormalFallacy(FormalFallacy),
|
||||
InformalFallacy(InformalFallacy),
|
||||
}
|
||||
impl Default for Fallacy {
|
||||
fn default() -> Self {
|
||||
Fallacy::FormalFallacy(FormalFallacy::PropositionalFallacy)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Fallacy {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Fallacy::FormalFallacy(formal) => write!(f, "Formal: {}", formal),
|
||||
Fallacy::InformalFallacy(informal) => write!(f, "Informal: {}", informal),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Fallacy {
|
||||
pub(crate) fn get_filename(&self) -> String {
|
||||
match self {
|
||||
Fallacy::FormalFallacy(formal) => format!("formal/{}", formal.get_filename()),
|
||||
Fallacy::InformalFallacy(informal) => format!("informal/{}", informal.get_filename()),
|
||||
}
|
||||
}
|
||||
pub(crate) fn random() -> Self {
|
||||
let mut rng = rng();
|
||||
let variants = [
|
||||
Fallacy::FormalFallacy(FormalFallacy::random()),
|
||||
Fallacy::InformalFallacy(InformalFallacy::random()),
|
||||
];
|
||||
variants.choose(&mut rng).unwrap().clone()
|
||||
}
|
||||
}
|
||||
/// Formal fallacies, which are errors in the structure of an argument.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum FormalFallacy {
|
||||
PropositionalFallacy,
|
||||
ProbabilisticFallacy,
|
||||
SyllogisticFallacy,
|
||||
QuantificationalFallacy
|
||||
}
|
||||
impl Default for FormalFallacy {
|
||||
fn default() -> Self {
|
||||
FormalFallacy::PropositionalFallacy
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for FormalFallacy {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FormalFallacy::PropositionalFallacy => write!(f, "Propositional Fallacy"),
|
||||
FormalFallacy::ProbabilisticFallacy => write!(f, "Probabilistic Fallacy"),
|
||||
FormalFallacy::SyllogisticFallacy => write!(f, "Syllogistic Fallacy"),
|
||||
FormalFallacy::QuantificationalFallacy => write!(f, "Quantificational Fallacy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FormalFallacy {
|
||||
pub(crate) fn get_filename(&self) -> String {
|
||||
match self {
|
||||
FormalFallacy::PropositionalFallacy => "propositional".to_string(),
|
||||
FormalFallacy::ProbabilisticFallacy => "probabilistic".to_string(),
|
||||
FormalFallacy::SyllogisticFallacy => "syllogistic".to_string(),
|
||||
FormalFallacy::QuantificationalFallacy => "quantificational".to_string(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn random() -> Self {
|
||||
let mut rng = rng();
|
||||
let variants = [
|
||||
FormalFallacy::PropositionalFallacy,
|
||||
FormalFallacy::ProbabilisticFallacy,
|
||||
FormalFallacy::SyllogisticFallacy,
|
||||
FormalFallacy::QuantificationalFallacy,
|
||||
];
|
||||
variants.choose(&mut rng).unwrap().clone()
|
||||
}
|
||||
}
|
||||
/// Informal fallacies, which are errors in reasoning that do not involve the structure of the argument.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum InformalFallacy {
|
||||
PostHocErgoPropterHoc,
|
||||
SlipperySlope,
|
||||
TexasSharpshooter,
|
||||
HastyGeneralization,
|
||||
OverGeneralization,
|
||||
NoTrueScotsman,
|
||||
QuotingOutOfContext,
|
||||
AdHominem,
|
||||
TuQuoque,
|
||||
Bandwagon,
|
||||
StrawMan,
|
||||
AdIgnorantiam,
|
||||
SpecialPleading,
|
||||
BeggingTheQuestion
|
||||
}
|
||||
impl Default for InformalFallacy {
|
||||
fn default() -> Self {
|
||||
InformalFallacy::PostHocErgoPropterHoc
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for InformalFallacy {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
InformalFallacy::PostHocErgoPropterHoc => write!(f, "Post Hoc Ergo Propter Hoc"),
|
||||
InformalFallacy::SlipperySlope => write!(f, "Slippery Slope"),
|
||||
InformalFallacy::TexasSharpshooter => write!(f, "Texas Sharpshooter"),
|
||||
InformalFallacy::HastyGeneralization => write!(f, "Hasty Generalization"),
|
||||
InformalFallacy::OverGeneralization => write!(f, "Over Generalization"),
|
||||
InformalFallacy::NoTrueScotsman => write!(f, "No True Scotsman"),
|
||||
InformalFallacy::QuotingOutOfContext => write!(f, "Quoting Out Of Context"),
|
||||
InformalFallacy::AdHominem => write!(f, "Ad Hominem"),
|
||||
InformalFallacy::TuQuoque => write!(f, "Tu Quoque"),
|
||||
InformalFallacy::Bandwagon => write!(f, "Bandwagon"),
|
||||
InformalFallacy::StrawMan => write!(f, "Straw Man"),
|
||||
InformalFallacy::AdIgnorantiam => write!(f, "Ad Ignorantiam"),
|
||||
InformalFallacy::SpecialPleading => write!(f, "Special Pleading"),
|
||||
InformalFallacy::BeggingTheQuestion => write!(f, "Begging The Question"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl InformalFallacy {
|
||||
pub(crate) fn get_filename(&self) -> String {
|
||||
match self {
|
||||
InformalFallacy::PostHocErgoPropterHoc => "post_hoc_ergo_propter_hoc".to_string(),
|
||||
InformalFallacy::SlipperySlope => "slippery_slope".to_string(),
|
||||
InformalFallacy::TexasSharpshooter => "texas_sharpshooter".to_string(),
|
||||
InformalFallacy::HastyGeneralization => "hasty_generalization".to_string(),
|
||||
InformalFallacy::OverGeneralization => "over_generalization".to_string(),
|
||||
InformalFallacy::NoTrueScotsman => "no_true_scotsman".to_string(),
|
||||
InformalFallacy::QuotingOutOfContext => "quoting_out_of_context".to_string(),
|
||||
InformalFallacy::AdHominem => "ad_hominem".to_string(),
|
||||
InformalFallacy::TuQuoque => "tu_quoque".to_string(),
|
||||
InformalFallacy::Bandwagon => "bandwagon".to_string(),
|
||||
InformalFallacy::StrawMan => "straw_man".to_string(),
|
||||
InformalFallacy::AdIgnorantiam => "ad_ignorantiam".to_string(),
|
||||
InformalFallacy::SpecialPleading => "special_pleading".to_string(),
|
||||
InformalFallacy::BeggingTheQuestion => "begging_the_question".to_string(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn random() -> Self {
|
||||
let mut rng = rng();
|
||||
let variants = [
|
||||
InformalFallacy::PostHocErgoPropterHoc,
|
||||
InformalFallacy::SlipperySlope,
|
||||
InformalFallacy::TexasSharpshooter,
|
||||
InformalFallacy::HastyGeneralization,
|
||||
InformalFallacy::OverGeneralization,
|
||||
InformalFallacy::NoTrueScotsman,
|
||||
InformalFallacy::QuotingOutOfContext,
|
||||
InformalFallacy::AdHominem,
|
||||
InformalFallacy::TuQuoque,
|
||||
InformalFallacy::Bandwagon,
|
||||
InformalFallacy::StrawMan,
|
||||
InformalFallacy::AdIgnorantiam,
|
||||
InformalFallacy::SpecialPleading,
|
||||
InformalFallacy::BeggingTheQuestion,
|
||||
];
|
||||
variants.choose(&mut rng).unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Bias module, which includes various lessons related to cognitive biases and their impact on reasoning.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum Bias {
|
||||
ConfirmationBias,
|
||||
TheHaloEffect,
|
||||
FundamentalAttributionError,
|
||||
InGroupBias,
|
||||
DunningKrugerEffect,
|
||||
BarnumEffect
|
||||
}
|
||||
impl Default for Bias {
|
||||
fn default() -> Self {
|
||||
Bias::ConfirmationBias
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Bias {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Bias::ConfirmationBias => write!(f, "Confirmation Bias"),
|
||||
Bias::TheHaloEffect => write!(f, "The Halo Effect"),
|
||||
Bias::FundamentalAttributionError => write!(f, "Fundamental Attribution Error"),
|
||||
Bias::InGroupBias => write!(f, "In Group Bias"),
|
||||
Bias::DunningKrugerEffect => write!(f, "Dunning Kruger Effect"),
|
||||
Bias::BarnumEffect => write!(f, "Barnum Effect"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Bias {
|
||||
pub(crate) fn get_filename(&self) -> String {
|
||||
match self {
|
||||
Bias::ConfirmationBias => "confirmation_bias".to_string(),
|
||||
Bias::TheHaloEffect => "the_halo_effect".to_string(),
|
||||
Bias::FundamentalAttributionError => "fundamental_attribution_error".to_string(),
|
||||
Bias::InGroupBias => "in_group_bias".to_string(),
|
||||
Bias::DunningKrugerEffect => "dunning_kruger_effect".to_string(),
|
||||
Bias::BarnumEffect => "barnum_effect".to_string(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn random() -> Self {
|
||||
let mut rng = rng();
|
||||
let variants = [
|
||||
Bias::ConfirmationBias,
|
||||
Bias::TheHaloEffect,
|
||||
Bias::FundamentalAttributionError,
|
||||
Bias::InGroupBias,
|
||||
Bias::DunningKrugerEffect,
|
||||
Bias::BarnumEffect,
|
||||
];
|
||||
variants.choose(&mut rng).unwrap().clone()
|
||||
}
|
||||
}
|
||||
/// Appraisal module, which includes various lessons related to the evaluation and appraisal of arguments.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum Appraisal {
|
||||
ConversionToPropositional,
|
||||
CounterArgument,
|
||||
}
|
||||
impl Default for Appraisal {
|
||||
fn default() -> Self {
|
||||
Appraisal::ConversionToPropositional
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Appraisal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Appraisal::ConversionToPropositional => write!(f, "Conversion to Propositional"),
|
||||
Appraisal::CounterArgument => write!(f, "Counter Argument"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Appraisal {
|
||||
pub(crate) fn get_filename(&self) -> String {
|
||||
match self {
|
||||
Appraisal::ConversionToPropositional => "conversion_to_propositional".to_string(),
|
||||
Appraisal::CounterArgument => "counter_argument".to_string(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn random() -> Self {
|
||||
let mut rng = rng();
|
||||
let variants = [
|
||||
Appraisal::ConversionToPropositional,
|
||||
Appraisal::CounterArgument,
|
||||
];
|
||||
variants.choose(&mut rng).unwrap().clone()
|
||||
}
|
||||
}
|
1
src/learner/mod.rs
Normal file
1
src/learner/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub(crate) mod profile;
|
94
src/learner/profile.rs
Normal file
94
src/learner/profile.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crossterm::style::Stylize;
|
||||
use crossterm::terminal::size;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::course::{units, tracker};
|
||||
use crate::{files, printers};
|
||||
use crate::printers::{clear_console, wait_for_input};
|
||||
use crate::utilities::questions;
|
||||
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Learner {
|
||||
pub name: String,
|
||||
pub filename: String,
|
||||
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
|
||||
pub progress: HashMap<units::Module, tracker::Tracker>,
|
||||
}
|
||||
|
||||
impl Learner {
|
||||
pub fn new() -> Self {
|
||||
let previous_saves = files::get_previous_saves();
|
||||
|
||||
let user = questions::ask_mcq(
|
||||
"Please select or create a user profile.",
|
||||
&previous_saves.iter().map(String::as_str).collect::<Vec<&str>>(),
|
||||
).unwrap();
|
||||
|
||||
let learner;
|
||||
|
||||
if user == "Create New" {
|
||||
let name = questions::ask_name();
|
||||
let sanitized_name = files::sanitize_name(name.as_str());
|
||||
let filename = format!("saves/{}.json", sanitized_name);
|
||||
let modules = units::Module::iter();
|
||||
let mut progress = HashMap::new();
|
||||
for module in modules {
|
||||
progress.insert(module.clone(), tracker::Tracker::new(module));
|
||||
}
|
||||
learner = Self {
|
||||
name: name.clone(),
|
||||
filename: filename.clone(),
|
||||
progress: progress.clone(),
|
||||
};
|
||||
files::save_to_file(&filename, &learner).expect("Failed to create new profile file");
|
||||
printers::print_boxed(&format!("Profile created for {}. Your progress will be saved in {}.", name, filename), printers::StatementType::Default);
|
||||
printers::print_boxed("You won't have any progress yet, but the next screen shows what your progress report will look like. Fill all the progress bars to win.", printers::StatementType::Default);
|
||||
} else {
|
||||
let sanitized_name = files::sanitize_name(&user);
|
||||
let filename = format!("saves/{}.json", sanitized_name);
|
||||
learner = files::load_from_file(&filename).expect("Failed to load profile file");
|
||||
printers::print_boxed(&format!("Profile loaded for {}.", user), printers::StatementType::Default);
|
||||
}
|
||||
learner.display_progress();
|
||||
return learner
|
||||
}
|
||||
|
||||
pub fn display_progress(&self) {
|
||||
|
||||
clear_console();
|
||||
|
||||
let (cols, _) = size().unwrap();
|
||||
let dashes = "-".repeat((cols as usize));
|
||||
let dots = " ".repeat((cols as usize).saturating_sub(42));
|
||||
|
||||
println!("Progress Report for {}:", self.name);
|
||||
println!("{}", dashes);
|
||||
println!("{}", format!(" Unit {} | Mastery |", dots).bold());
|
||||
|
||||
let mut modules = units::Module::iter();
|
||||
for module in modules {
|
||||
if let Some(progress) = self.progress.get(&module) {
|
||||
self.progress.get(&module).unwrap().print_progress();
|
||||
}
|
||||
}
|
||||
|
||||
// let mut sorted_progress: Vec<_> = self.progress.order();
|
||||
// // sorted_progress.sort_by_key(|(module, _)| module.to_string());
|
||||
// for (_, tracker) in sorted_progress {
|
||||
// tracker.print_progress();
|
||||
// }
|
||||
|
||||
println!("{}", dashes);
|
||||
|
||||
wait_for_input();
|
||||
}
|
||||
|
||||
pub fn update_progress(&mut self, unit: units::Module, question_correct: bool) {
|
||||
if let Some(tracker) = self.progress.get_mut(&unit) {
|
||||
tracker.quiz_result(question_correct);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
19
src/main.rs
Normal file
19
src/main.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
mod learner;
|
||||
mod utilities;
|
||||
mod course;
|
||||
|
||||
use course::{story, units::Module};
|
||||
use learner::profile::Learner;
|
||||
use utilities::{files, printers, printers::StatementType};
|
||||
|
||||
fn main() {
|
||||
|
||||
let mut learner = Learner::new();
|
||||
|
||||
printers::print_boxed(
|
||||
&format!("Welcome, {}! Thanks for joining me.", learner.name),
|
||||
StatementType::Default );
|
||||
|
||||
story::outer_loop(&mut learner);
|
||||
|
||||
}
|
122
src/utilities/files.rs
Normal file
122
src/utilities/files.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::{course::story, learner::profile::{self, Learner}};
|
||||
|
||||
// pub fn save_learner_progress(learner: &Learner, file: &str) -> Result<(), std::io::Error> {
|
||||
// learner.clone().save_to_file(file)?;
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
pub fn get_previous_saves() -> Vec<String> {
|
||||
let mut previous_saves = std::fs::read_dir("saves/")
|
||||
.unwrap_or_else(|_| {
|
||||
std::fs::create_dir_all("saves/").expect("Failed to create the saves/ directory");
|
||||
std::fs::read_dir("saves/").expect("Failed to read the saves/ directory")
|
||||
})
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.extension()?.to_str()? == "json" {
|
||||
let file_content = std::fs::read_to_string(&path).ok()?;
|
||||
let learner: Learner = serde_json::from_str(&file_content).ok()?;
|
||||
Some(learner.name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
previous_saves.push("Create New".to_string());
|
||||
previous_saves
|
||||
}
|
||||
|
||||
pub fn save_to_file(file: &str, learner: &Learner) -> Result<String, std::io::Error> {
|
||||
match serde_json::to_string_pretty(learner) {
|
||||
Ok(json_data) => match std::fs::write(file, json_data) {
|
||||
Ok(_) => Ok(format!("File saved to {}", file.to_string())),
|
||||
Err(e) => {
|
||||
eprintln!("Error writing to file: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Error serializing learner: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_from_file<T: DeserializeOwned>(filename: &str) -> Result<T, Box<dyn std::error::Error>> {
|
||||
let mut file = File::open(filename)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let data = serde_json::from_str(&contents)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
// pub fn load_learner_progress() -> Result<Learner, std::io::Error> {
|
||||
// let mut new_session: bool = false;
|
||||
|
||||
// let mut learner = {
|
||||
// // Initialize the learner's progress
|
||||
// // Check if a save file exists to load previous progress
|
||||
// if std::path::Path::new(SAVE_FILE).exists() {
|
||||
// match Learner::load_from_file(SAVE_FILE) {
|
||||
// Ok(progress) => {
|
||||
// print_boxed(&format!("Welcome back, {}! Let's continue your journey in FABLE.", progress.name), StatementType::Default );
|
||||
// progress
|
||||
// }
|
||||
// Err(e) => {
|
||||
// new_session = true;
|
||||
// print_boxed(&format!("Couldn't load your save file due to error: {}.\nStarting a new learning experience.", e), StatementType::IncorrectFeedback );
|
||||
// Learner::initialize_learner()
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// new_session = true;
|
||||
// // Start a new session
|
||||
// print_boxed("
|
||||
// Welcome to FABLE, an interactive tutor for formal reasoning.
|
||||
// Fallacy ~~~~~~~~~~~~
|
||||
// Awareness and ~~~~~~
|
||||
// Bias ~~~~~~~~~~~~~~~
|
||||
// Learning ~~~~~~~~~~~
|
||||
// Environment ~~~~~~~~", StatementType::Default);
|
||||
// print_boxed("
|
||||
// My name is Francis, and I am here to help you learn about formal reasoning.\n
|
||||
// You see, many years ago, my people were nearly wiped out because of a lack of critical thinking.
|
||||
// I have been programmed to help you avoid the same fate.\n
|
||||
// In the next screen, you can select what you want to study.
|
||||
// Choose wisely. The fate of your people depends on it.", StatementType::Default);
|
||||
// Learner::initialize_learner()
|
||||
// }
|
||||
// };
|
||||
// if !new_session {
|
||||
// learner.display_progress();
|
||||
// learner.confirm_course_selection(); // Confirm the course selection after reading the progress
|
||||
// }
|
||||
|
||||
// let json_data = std::fs::read_to_string(SAVE_FILE)?;
|
||||
// let progress: Learner = serde_json::from_str(&json_data)?;
|
||||
// Ok(progress)
|
||||
// }
|
||||
|
||||
|
||||
// pub fn load_learner_progress() -> Result<Learner, std::io::Error> {
|
||||
// let json_data = fs::read_to_string(SAVE_FILE)?;
|
||||
// let progress: Learner = serde_json::from_str(&json_data)?;
|
||||
// Ok(progress)
|
||||
// }
|
||||
|
||||
pub fn sanitize_name(name: &str) -> String {
|
||||
name.replace(|c: char| !c.is_alphanumeric(), "_").to_lowercase()
|
||||
}
|
||||
|
||||
pub fn parse_unit(filename: &str) -> Result<story::UnitStory, Box<dyn std::error::Error>> {
|
||||
let mut file = File::open(filename)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let data = serde_json::from_str(&contents)?;
|
||||
Ok(data)
|
||||
}
|
123
src/utilities/generators.rs
Normal file
123
src/utilities/generators.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use rand::Rng;
|
||||
|
||||
/// Syllables that can make up a name.
|
||||
/// WARNING: Later in this file, there is a list of offensive terms that should not be included in the name.
|
||||
/// If you add more syllables, make sure they do not form any offensive terms.
|
||||
const SYLLABLES: [&str; 295] = [
|
||||
"ba", "be", "bi", "bo", "bu",
|
||||
"ca", "ce", "ci", "co", "cu",
|
||||
"da", "de", "di", "do", "du",
|
||||
"fa", "fe", "fi", "fo", "fu",
|
||||
"ga", "ge", "gi", "go", "gu",
|
||||
"ha", "he", "hi", "ho", "hu",
|
||||
"ja", "je", "ji", "jo", "ju",
|
||||
"ka", "ke", "ki", "ko", "ku",
|
||||
"la", "le", "li", "lo", "lu",
|
||||
"ma", "me", "mi", "mo", "mu",
|
||||
"na", "ne", "ni", "no", "nu",
|
||||
"pa", "pe", "pi", "po", "pu",
|
||||
"qa", "qe", "qi", "qo", "qu",
|
||||
"ra", "re", "ri", "ro", "ru",
|
||||
"sa", "se", "si", "so", "su",
|
||||
"ta", "te", "ti", "to", "tu",
|
||||
"va", "ve", "vi", "vo", "vu",
|
||||
"wa", "we", "wi", "wo", "wu",
|
||||
"xa", "xe", "xi", "xo", "xu",
|
||||
"ya", "ye", "yi", "yo", "yu",
|
||||
"za", "ze", "zi", "zo", "zu",
|
||||
"bai", "bau", "bei", "beu", "boi", "bou",
|
||||
"cai", "cau", "cei", "ceu", "coi", "cou",
|
||||
"dai", "dau", "dei", "deu", "doi", "dou",
|
||||
"fai", "fau", "fei", "feu", "foi", "fou",
|
||||
"gai", "gau", "gei", "geu", "goi", "gou",
|
||||
"hai", "hau", "hei", "heu", "hoi", "hou",
|
||||
"jai", "jau", "jei", "jeu", "joi", "jou",
|
||||
"kai", "kau", "kei", "keu", "koi", "kou",
|
||||
"lai", "lau", "lei", "leu", "loi", "lou",
|
||||
"mai", "mau", "mei", "meu", "moi", "mou",
|
||||
"nai", "nau", "nei", "neu", "noi", "nou",
|
||||
"pai", "pau", "pei", "peu", "poi", "pou",
|
||||
"qai", "qau", "qei", "qeu", "qoi", "qou",
|
||||
"rai", "rau", "rei", "reu", "roi", "rou",
|
||||
"sai", "sau", "sei", "seu", "soi", "sou",
|
||||
"tai", "tau", "tei", "teu", "toi", "tou",
|
||||
"vai", "vau", "vei", "veu", "voi", "vou",
|
||||
"wai", "wau", "wei", "weu", "woi", "wou",
|
||||
"xai", "xau", "xei", "xeu", "xoi", "xou",
|
||||
"yai", "yau", "yei", "yeu", "yoi", "you",
|
||||
"zai", "zau", "zei", "zeu", "zoi", "zou",
|
||||
"cha", "che", "chi", "cho", "chu",
|
||||
"sha", "she", "shi", "sho", "shu",
|
||||
"tha", "the", "thi", "tho", "thu",
|
||||
"wha", "whe", "whi", "who", "whu",
|
||||
"pha", "phe", "phi", "pho", "phu",
|
||||
"zha", "zhe", "zhi", "zho", "zhu",
|
||||
"tra", "tre", "tri", "tro", "tru",
|
||||
"kha", "khe", "khi", "kho", "khu",
|
||||
"gha", "ghe", "ghi", "gho", "ghu",
|
||||
"sha", "she", "shi", "sho", "shu",
|
||||
"twa", "twe", "twi", "two", "twu",
|
||||
"qua", "que", "qui", "quo",
|
||||
"sla", "sle", "sli", "slo", "slu",
|
||||
];
|
||||
|
||||
const OFFENSIVE_TERMS: [&str; 20] = [
|
||||
"shit", "fat", "whore", "nig",
|
||||
"jig", "cum", "fag", "twat",
|
||||
"jiz", "kum", "anal", "quere",
|
||||
"phuk", "fuk", "arab", "dik",
|
||||
"jew", "slut", "tit", "phag"
|
||||
];
|
||||
|
||||
const affirmations: [&str; 8] = [
|
||||
"You're doing great!",
|
||||
"Keep up the good work!",
|
||||
"Fantastic effort!",
|
||||
"You're on the right track!",
|
||||
"Excellent job!",
|
||||
"You're making progress!",
|
||||
"Keep it up!",
|
||||
"Great work!",
|
||||
];
|
||||
|
||||
pub fn random_affirmation() -> String {
|
||||
let mut rng = rand::rng();
|
||||
let index = rng.random_range(0..affirmations.len());
|
||||
affirmations[index].to_string()
|
||||
}
|
||||
|
||||
pub fn generate_name() -> String {
|
||||
|
||||
let mut rng = rand::rng();
|
||||
let name_length = rng.random_range(2..5); // Generate a name with 2 to 4 syllables
|
||||
let mut name = String::new();
|
||||
|
||||
for _ in 0..name_length {
|
||||
let syllable = SYLLABLES[rng.random_range(0..SYLLABLES.len())];
|
||||
name.push_str(syllable);
|
||||
}
|
||||
|
||||
if OFFENSIVE_TERMS.iter().any(|&term| name.contains(term)) {
|
||||
return generate_name(); // Regenerate the name if it contains offensive terms
|
||||
}
|
||||
|
||||
if name.len() > 8 {
|
||||
return generate_name(); // Regenerate the name if it contains too many characters
|
||||
}
|
||||
|
||||
name[..1].to_ascii_uppercase() + &name[1..]
|
||||
}
|
||||
|
||||
|
||||
pub fn random_unit_intro() -> String {
|
||||
let mut rng = rand::rng();
|
||||
let unit_intros: [&str; 5] = [
|
||||
"It's time to learn about",
|
||||
"Let's dive into",
|
||||
"Get ready to explore",
|
||||
"Prepare to discover",
|
||||
"Let's embark on a journey to learn about",
|
||||
];
|
||||
let index = rng.random_range(0..unit_intros.len());
|
||||
unit_intros[index].to_string()
|
||||
}
|
4
src/utilities/mod.rs
Normal file
4
src/utilities/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub(crate) mod generators;
|
||||
pub(crate) mod files;
|
||||
pub(crate) mod printers;
|
||||
pub(crate) mod questions;
|
271
src/utilities/printers.rs
Normal file
271
src/utilities/printers.rs
Normal file
@@ -0,0 +1,271 @@
|
||||
use crossterm::{execute, style::Stylize, terminal::{size, Clear, ClearType}};
|
||||
use std::io::stdout;
|
||||
|
||||
use super::generators;
|
||||
|
||||
const DELAY_SECONDS: u64 = 1;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum StatementType {
|
||||
Default,
|
||||
DelayExplanation,
|
||||
Question,
|
||||
GeneralFeedback,
|
||||
CorrectFeedback,
|
||||
IncorrectFeedback,
|
||||
}
|
||||
|
||||
/// Clear the console
|
||||
pub fn clear_console() {
|
||||
execute!(stdout(), Clear(ClearType::All)).unwrap();
|
||||
}
|
||||
|
||||
/// Pretty-print an instruction centered on the screen in an outlined box
|
||||
pub fn print_boxed(instruction: &str, statement_type: StatementType) {
|
||||
let (cols, rows) = size().unwrap();
|
||||
let padding = 4;
|
||||
let max_width = 100;
|
||||
let instruction = instruction
|
||||
.lines()
|
||||
.flat_map(|line| {
|
||||
let mut split_lines = vec![];
|
||||
let mut current_line = String::new();
|
||||
for word in line.split_whitespace() {
|
||||
let word_width = word.chars().map(|c| if c.len_utf8() > 1 { 2 } else { 1 }).sum::<usize>();
|
||||
if current_line.chars().map(|c| if c.len_utf8() > 1 { 2 } else { 1 }).sum::<usize>() + word_width + 1 > std::cmp::min(max_width, cols as usize) {
|
||||
split_lines.push(current_line.trim_end().to_string());
|
||||
current_line = String::new();
|
||||
}
|
||||
current_line.push_str(word);
|
||||
current_line.push(' ');
|
||||
}
|
||||
if !current_line.is_empty() {
|
||||
split_lines.push(current_line.trim_end().to_string());
|
||||
}
|
||||
split_lines
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let box_width = std::cmp::min(
|
||||
instruction.lines().map(|line| line.chars().map(|c| c.len_utf8()).sum::<usize>()).max().unwrap_or(0) + padding * 2,
|
||||
max_width,
|
||||
);
|
||||
let box_height = instruction.lines().count() + 2;
|
||||
|
||||
let start_col = (cols.saturating_sub(box_width as u16)) / 2;
|
||||
let start_row = (rows.saturating_sub(box_height as u16)) / 2;
|
||||
|
||||
let top_border;
|
||||
let bottom_border;
|
||||
let empty_line;
|
||||
|
||||
match statement_type {
|
||||
StatementType::Question => {
|
||||
top_border = format!("{}{}{}", "┌".dark_blue(), "─".repeat(box_width).dark_blue(), "┐".dark_blue());
|
||||
bottom_border = format!("{}{}{}", "└".dark_blue(), "─".repeat(box_width).dark_blue(), "┘".dark_blue());
|
||||
empty_line = format!("{}{}{}", "│".dark_blue(), " ".repeat(box_width).dark_blue(), "│".dark_blue());
|
||||
}
|
||||
StatementType::GeneralFeedback => {
|
||||
top_border = format!("{}{}{}", "┌".dark_magenta(), "─".repeat(box_width).dark_magenta(), "┐".dark_magenta());
|
||||
bottom_border = format!("{}{}{}", "└".dark_magenta(), "─".repeat(box_width).dark_magenta(), "┘".dark_magenta());
|
||||
empty_line = format!("{}{}{}", "│".dark_magenta(), " ".repeat(box_width).dark_magenta(), "│".dark_magenta());
|
||||
}
|
||||
StatementType::CorrectFeedback => {
|
||||
top_border = format!("{}{}{}", "┌".green(), "─".repeat(box_width).green(), "┐".green());
|
||||
bottom_border = format!("{}{}{}", "└".green(), "─".repeat(box_width).green(), "┘".green());
|
||||
empty_line = format!("{}{}{}", "│".green(), " ".repeat(box_width).green(), "│".green());
|
||||
}
|
||||
StatementType::IncorrectFeedback => {
|
||||
top_border = format!("{}{}{}", "┌".red(), "─".repeat(box_width).red(), "┐".red());
|
||||
bottom_border = format!("{}{}{}", "└".red(), "─".repeat(box_width).red(), "┘".red());
|
||||
empty_line = format!("{}{}{}", "│".red(), " ".repeat(box_width).red(), "│".red());
|
||||
}
|
||||
_ => {
|
||||
top_border = format!("┌{}┐", "─".repeat(box_width));
|
||||
bottom_border = format!("└{}┘", "─".repeat(box_width));
|
||||
empty_line = format!("│{}│", " ".repeat(box_width));
|
||||
}
|
||||
}
|
||||
|
||||
clear_console();
|
||||
|
||||
for i in 0..box_height {
|
||||
let line = match i {
|
||||
0 => &top_border,
|
||||
i if i == box_height - 1 => &bottom_border,
|
||||
i if i > 0 && i < box_height - 1 => {
|
||||
let line_content = instruction
|
||||
.lines()
|
||||
.nth(i - 1)
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
let line_width = line_content.chars().map(|c| if c.len_utf8() > 1 { 2 } else { 1 }).sum::<usize>();
|
||||
match statement_type {
|
||||
StatementType::Question => {
|
||||
&format!(
|
||||
"{}{}{}{}{}",
|
||||
"│".dark_blue(),
|
||||
" ".repeat((box_width.saturating_sub(line_width)) / 2),
|
||||
line_content,
|
||||
" ".repeat((box_width.saturating_sub(line_width) + 1) / 2),
|
||||
"│".dark_blue(),
|
||||
)
|
||||
}
|
||||
StatementType::GeneralFeedback => {
|
||||
&format!(
|
||||
"{}{}{}{}{}",
|
||||
"│".dark_magenta(),
|
||||
" ".repeat((box_width.saturating_sub(line_width)) / 2),
|
||||
line_content,
|
||||
" ".repeat((box_width.saturating_sub(line_width) + 1) / 2),
|
||||
"│".dark_magenta(),
|
||||
)
|
||||
}
|
||||
StatementType::CorrectFeedback => {
|
||||
&format!(
|
||||
"{}{}{}{}{}",
|
||||
"│".green(),
|
||||
" ".repeat((box_width.saturating_sub(line_width)) / 2),
|
||||
line_content,
|
||||
" ".repeat((box_width.saturating_sub(line_width) + 1) / 2),
|
||||
"│".green(),
|
||||
)
|
||||
}
|
||||
StatementType::IncorrectFeedback => {
|
||||
&format!(
|
||||
"{}{}{}{}{}",
|
||||
"│".red(),
|
||||
" ".repeat((box_width.saturating_sub(line_width)) / 2),
|
||||
line_content,
|
||||
" ".repeat((box_width.saturating_sub(line_width) + 1) / 2),
|
||||
"│".red(),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
&format!(
|
||||
"│{}{}{}│",
|
||||
" ".repeat((box_width.saturating_sub(line_width)) / 2),
|
||||
line_content,
|
||||
" ".repeat((box_width.saturating_sub(line_width) + 1) / 2),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => &empty_line,
|
||||
};
|
||||
println!("\x1B[{};{}H{}", start_row + i as u16, start_col + 1, line);
|
||||
}
|
||||
|
||||
match statement_type {
|
||||
StatementType::Question => {
|
||||
return;
|
||||
}
|
||||
StatementType::DelayExplanation => {
|
||||
wait_for_input_delay(DELAY_SECONDS);
|
||||
print_boxed(&instruction, StatementType::Default);
|
||||
}
|
||||
_ => {
|
||||
wait_for_input();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints a full-width ruler of designated characters
|
||||
pub fn print_rule(character: char, padding: (bool, bool)) {
|
||||
let (cols, _) = size().unwrap();
|
||||
|
||||
println!(
|
||||
"{}{}{}",
|
||||
if padding.0 { "\n" } else { "" },
|
||||
character.to_string().repeat(cols as usize),
|
||||
if padding.1 { "\n" } else { "" }
|
||||
);
|
||||
}
|
||||
|
||||
/// Horizontally center the desired text on the screen
|
||||
pub fn print_centered(text: &str) {
|
||||
let (cols, _) = size().unwrap();
|
||||
let text_width = text.len() as u16;
|
||||
let start_col = (cols.saturating_sub(text_width)) / 2;
|
||||
|
||||
println!(
|
||||
"\x1B[{}C{}",
|
||||
start_col,
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
/// Wait for input from the user
|
||||
pub fn wait_for_input() {
|
||||
let prompt = "Press enter to continue";
|
||||
let (cols, _) = size().unwrap();
|
||||
let text_width = prompt.len() as u16;
|
||||
let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
|
||||
|
||||
println!(
|
||||
"\x1B[{}C{}",
|
||||
start_col,
|
||||
prompt.italic().dark_blue()
|
||||
);
|
||||
|
||||
// println!("{}", prompt.italic().dark_blue());
|
||||
crossterm::event::read().unwrap();
|
||||
}
|
||||
|
||||
/// Wait for input from the user
|
||||
pub fn print_title(title: &str) {
|
||||
let (cols, _) = size().unwrap();
|
||||
let text_width = title.len() as u16;
|
||||
let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
|
||||
|
||||
println!(
|
||||
"\x1B[{}C{}",
|
||||
start_col,
|
||||
title.bold()
|
||||
);
|
||||
|
||||
wait_for_input();
|
||||
}
|
||||
|
||||
/// Wait for input from the user
|
||||
pub fn wait_for_input_delay(seconds: u64) {
|
||||
let prompt = format!("Remember to be mindful. Advance in {} seconds.", seconds);
|
||||
let (cols, _) = size().unwrap();
|
||||
let text_width = prompt.len() as u16;
|
||||
let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
|
||||
|
||||
println!(
|
||||
"\x1B[{}C{}",
|
||||
start_col,
|
||||
prompt.italic().dark_blue()
|
||||
);
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(DELAY_SECONDS));
|
||||
|
||||
}
|
||||
|
||||
/// Wait for input from the user
|
||||
pub fn inform_delay(seconds: u64) {
|
||||
let prompt = format!("Remember to be mindful. Answers appear in {} seconds.", seconds);
|
||||
let (cols, _) = size().unwrap();
|
||||
let text_width = prompt.len() as u16;
|
||||
let start_col = (cols.saturating_sub(text_width)) / 2 + 1;
|
||||
|
||||
println!(
|
||||
"\x1B[{}C{}",
|
||||
start_col,
|
||||
prompt.italic().dark_blue()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
pub fn unit_screen(title: &str) {
|
||||
clear_console();
|
||||
print_boxed(title, StatementType::GeneralFeedback);
|
||||
// print_boxed(&format!("{}", description), StatementType::Default);
|
||||
}
|
||||
|
||||
pub fn print_screen(text: &str) {
|
||||
clear_console();
|
||||
print_boxed(text, StatementType::Default);
|
||||
}
|
97
src/utilities/questions.rs
Normal file
97
src/utilities/questions.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use inquire::{MultiSelect, Select, Text};
|
||||
use crossterm::terminal;
|
||||
|
||||
use super::printers::{clear_console, inform_delay, print_boxed, StatementType};
|
||||
|
||||
const DELAY_SECONDS: u64 = 1;
|
||||
|
||||
pub fn ask_mcq_delayed(question: &str, options: &[&str]) -> Option<String> {
|
||||
print_boxed(question, StatementType::Question);
|
||||
inform_delay(DELAY_SECONDS);
|
||||
std::thread::sleep(std::time::Duration::from_secs(DELAY_SECONDS));
|
||||
ask_mcq(question, options)
|
||||
}
|
||||
|
||||
/// Print a quiz question in an attractive format and get the user's choice
|
||||
pub fn ask_mcq(question: &str, options: &[&str]) -> Option<String> {
|
||||
|
||||
clear_console();
|
||||
|
||||
// Calculate vertical centering
|
||||
let rows = terminal::size().map(|(_, height)| height as usize).unwrap_or(0) - options.len() - 2;
|
||||
|
||||
print_boxed(&format!("{}", question), StatementType::Question);
|
||||
|
||||
for _ in 0..(rows/2) {
|
||||
println!();
|
||||
}
|
||||
|
||||
let ans = Select::new("Your response:", options.to_vec())
|
||||
.prompt();
|
||||
|
||||
match ans {
|
||||
Ok(choice) => Some(String::from(choice)),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Print a quiz question in an attractive format and get the user's choices
|
||||
pub fn ask_multi_select(question: &str, options: &[&str]) -> Option<Vec<String>> {
|
||||
|
||||
clear_console();
|
||||
|
||||
// Calculate vertical centering
|
||||
let rows = terminal::size().map(|(_, height)| height as usize).unwrap_or(0) - options.len() - 2;
|
||||
|
||||
print_boxed(&format!("{}", question), StatementType::Question);
|
||||
|
||||
for _ in 0..(rows/2) {
|
||||
println!();
|
||||
}
|
||||
|
||||
let ans = MultiSelect::new("Select all applicable answers with SPACEBAR, then press RETURN to submit:", options.to_vec())
|
||||
.prompt();
|
||||
|
||||
match ans {
|
||||
Ok(choices) => Some(choices.into_iter().map(String::from).collect()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Print a question in an attractive format and get the user's text input
|
||||
pub fn ask_plaintext(question: &str) -> Option<String> {
|
||||
|
||||
clear_console();
|
||||
|
||||
// Calculate vertical centering
|
||||
let rows = terminal::size().map(|(_, height)| height as usize).unwrap_or(0) - 3;
|
||||
|
||||
print_boxed(&format!("{}", question), StatementType::Question);
|
||||
|
||||
for _ in 0..(rows/2) {
|
||||
println!();
|
||||
}
|
||||
|
||||
let ans = Text::new("Please provide your input:")
|
||||
.prompt();
|
||||
|
||||
match ans {
|
||||
Ok(choice) => Some(choice),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ask_name() -> String {
|
||||
let name = ask_plaintext("What is your name?").unwrap_or_else(|| {
|
||||
let default_name = super::generators::generate_name();
|
||||
print_boxed(&format!("Failed to get name. I'll give you a random one: {}.", default_name), StatementType::IncorrectFeedback);
|
||||
default_name
|
||||
});
|
||||
if name .is_empty() {
|
||||
let default_name = super::generators::generate_name();
|
||||
print_boxed(&format!("You didn't provide a name. I'll give you a random one: {}.", default_name), StatementType::IncorrectFeedback);
|
||||
return default_name;
|
||||
}
|
||||
print_boxed(&format!("Welcome, {}! Thanks for joining me.", name), StatementType::Default);
|
||||
name
|
||||
}
|
Reference in New Issue
Block a user