diff --git a/animals.txt b/animals.txt new file mode 100644 index 0000000..ae08b7c --- /dev/null +++ b/animals.txt @@ -0,0 +1,21 @@ +0,1,2,Does it quack? +1,0,0,duck +2,3,4,Is it wild? +3,5,6,Does it fly? +4,15,16,Does it woof? +5,7,8,Is it colourful? +6,13,14,Does it eat acorns? +7,0,0,parrot +8,9,10,Does it have a big beak? +9,11,12,Does it swim? +10,0,0,raven +11,0,0,swan +12,0,0,eagle +13,0,0,boar +14,0,0,wolf +15,0,0,dog +16,17,18,Does it give milk? +17,19,20,Is it grown for meat? +18,0,0,pig +19,0,0,cow +20,0,0,goat diff --git a/src/animals.rs b/src/animals.rs index a8c7d7c..3e576dd 100644 --- a/src/animals.rs +++ b/src/animals.rs @@ -1,8 +1,179 @@ -use crate::slot::Slot; use std::rc::Rc; +use std::io; +use crate::slot::Slot; -#[derive(Debug)] -enum NodeType { +pub trait UserAPI { + fn notify_new_game(&self); + fn notify_game_ended(&self); + fn notify_new_animal(&self, animal: &str); + fn notify_victory(&self); + fn answer_yes_no(&self, question: &str) -> bool; + fn is_it_a(&self, animal: &str) -> bool; + fn what_is_it(&self) -> String; + fn how_to_tell_apart(&self, secret: &str, other: &str) -> (String, bool); +} + +// --- storage --- + +mod storage { + use std::io; + use std::io::Read; + use std::io::Write; + use std::collections::HashMap; + use std::fs::File; + + use super::NodeType; + + #[derive(Debug)] + pub struct AnimalStorageEntry { + pub kind: NodeType, + pub text: String, + pub yes_index: i32, + pub no_index: i32, + } + + impl AnimalStorageEntry { + pub fn new(kind: NodeType, text: String, yes_index: i32, no_index: i32) -> Self { + AnimalStorageEntry { + kind, + text, + yes_index, + no_index, + } + } + } + + #[derive(Debug)] + pub struct AnimalStorage { + entries: HashMap, + next_free: i32, + } + + impl AnimalStorage { + pub fn new() -> Self { + AnimalStorage { + entries: HashMap::new(), + next_free: 0, + } + } + + pub fn reserve(&mut self) -> i32 { + let index = self.next_free; + self.next_free += 1; + index + } + + pub fn put(&mut self, pos: i32, entry: AnimalStorageEntry) { + assert_eq!(false, self.entries.contains_key(&pos)); + self.entries.insert(pos, entry); + } + + pub fn get(&self, pos: i32) -> Option<&AnimalStorageEntry> { + return self.entries.get(&pos); + } + + pub fn to_file(&self, path: &str) -> io::Result<()> { + let mut buf = String::new(); + + let mut sorted: Vec<_> = self.entries.iter().collect(); + sorted.sort_by(|a, b| a.0.cmp(b.0)); + + for (n, e) in sorted { + buf.push_str(&format!( + "{},{},{},{}\n", + n, e.yes_index, e.no_index, e.text + )); + } + + let mut file = File::create(path)?; + file.write_all(buf.as_bytes())?; + + Ok(()) + } + + pub fn from_file(path: &str) -> io::Result { + let mut store = Self::new(); + let mut s = String::new(); + File::open(path)?.read_to_string(&mut s)?; + + let lines = s.lines() + .map(|x| x.trim()) + .filter(|x| !x.is_empty()); + + for line in lines { + let pieces: Vec<&str> = line.splitn(4, ',').collect(); + + if let [num, yes, no, text] = pieces[..] { + let num: i32 = num.parse().unwrap(); + let yes: i32 = yes.parse().unwrap(); + let no: i32 = no.parse().unwrap(); + let kind = match (yes, no) { + (0, 0) => NodeType::Animal, + (_x, _y) if (_x == 0 || _y == 0) => panic!(format!("Structural error in file: {}", line)), + _ => NodeType::Question, + }; + store.put(num, AnimalStorageEntry::new(kind, text.to_string(), yes, no)); + } else { + panic!(format!("Syntax error in file: {}", line)); + } + } + + Ok(store) + } + } + + pub trait StorageLoadStore { + fn store(&self, store: &mut AnimalStorage, pos: i32); + fn load(&mut self, store: &mut AnimalStorage, pos: i32); + } + + impl StorageLoadStore for super::NodeStruct { + fn store(&self, store: &mut AnimalStorage, pos: i32) { + let yes_i = if self.branch_true.is_empty() { 0 } else { store.reserve() }; + let no_i = if self.branch_false.is_empty() { 0 } else { store.reserve() }; + + store.put( + pos, + AnimalStorageEntry::new(self.kind, self.text.clone(), yes_i, no_i), + ); + + println!("Write: {:?}, {} -> y {}, n {}", self.kind, self.text, yes_i, no_i); + + if yes_i != 0 { + self.branch_true.lease().store(store, yes_i); + } + + if no_i != 0 { + self.branch_false.lease().store(store, no_i); + } + } + + fn load(&mut self, store: &mut AnimalStorage, pos: i32) { + let entry = store.get(pos).unwrap(); + self.text = entry.text.clone(); + self.kind = entry.kind; + let yes_index = entry.yes_index; + let no_index = entry.no_index; + + if self.kind == NodeType::Question { + let mut true_node = super::NodeStruct::new(); + true_node.load(store, yes_index); + self.branch_true.put(true_node); + + let mut false_node = super::NodeStruct::new(); + false_node.load(store, no_index); + self.branch_false.put(false_node); + } + } + } +} + +use self::storage::StorageLoadStore; // take the trait into scope + +// --- node structure --- + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum NodeType { Question, Animal, } @@ -15,41 +186,53 @@ struct NodeStruct { branch_false: Slot, } -pub struct AnimalDB { - root: Slot, - enable_debug: bool, +impl NodeStruct { + pub fn new() -> NodeStruct { + NodeStruct { + kind: NodeType::Question, + text: "".to_string(), + branch_true: Slot::new(), + branch_false: Slot::new() + } + } } -pub trait UserAPI { - fn notify_new_game(&self); - fn notify_game_ended(&self); - fn notify_new_animal(&self, animal: &str); - fn notify_victory(&self); - fn answer_yes_no(&self, question: &str) -> bool; - fn is_it_a(&self, animal: &str) -> bool; - fn what_is_it(&self) -> String; - fn how_to_tell_apart(&self, other: &str) -> (String, bool); +// --- animal DB --- + +#[derive(Debug)] +pub struct AnimalDB { + root: Slot, } impl AnimalDB { pub fn new() -> AnimalDB { AnimalDB { root: Slot::new(), - enable_debug: false, } } - #[allow(dead_code)] - pub fn enable_debug(&mut self, yes: bool) { - self.enable_debug = yes; - } + pub fn save(&self, path: &str) -> io::Result<()> { + let mut store = storage::AnimalStorage::new(); + let n = store.reserve(); + self.root.lease().store(&mut store, n); - fn debug(&self, s: &str) { - if self.enable_debug { - println!("{}", s); + if !path.is_empty() { + store.to_file(path) + } else { + println!("{:#?}", store); + Ok(()) } } + pub fn load(&self, path: &str) -> io::Result<()> { + let mut store = storage::AnimalStorage::from_file(path)?; + let mut root_node = NodeStruct::new(); + root_node.load(&mut store, 0); + self.root.put(root_node); + + Ok(()) + } + fn insert_new_animal( &self, user: &dyn UserAPI, @@ -57,22 +240,7 @@ impl AnimalDB { updated_branch: &Slot, ) { let new_name = user.what_is_it(); - let (question, answer_for_new) = user.how_to_tell_apart(&following.text); - - self.debug(&format!( - "Updating DB with: ({}, y:{}, n:{})", - question, - if answer_for_new { - &new_name - } else { - &following.text - }, - if answer_for_new { - &following.text - } else { - &new_name - } - )); + let (question, answer_for_new) = user.how_to_tell_apart(&new_name, &following.text); // we have to insert a new Question node between the parent node and the // Animal node we just found @@ -92,11 +260,9 @@ impl AnimalDB { false => (&new_q.branch_false, &new_q.branch_true), }; - user.notify_new_animal(&new_name); - let new_animal = NodeStruct { kind: NodeType::Animal, - text: new_name, + text: new_name.clone(), branch_true: Slot::new(), branch_false: Slot::new(), }; @@ -106,6 +272,8 @@ impl AnimalDB { assert!(updated_branch.is_empty()); updated_branch.put(new_q); + + user.notify_new_animal(&new_name); } fn play_game(&self, user: &dyn UserAPI) { @@ -162,7 +330,6 @@ impl AnimalDB { if self.root.is_empty() { let name = user.what_is_it(); - self.debug(&format!("Initializing empty DB with: ({})", name)); self.root.put(NodeStruct { kind: NodeType::Animal, diff --git a/src/cli_user.rs b/src/cli_user.rs new file mode 100644 index 0000000..f2cbc28 --- /dev/null +++ b/src/cli_user.rs @@ -0,0 +1,76 @@ +use crate::animals; +use crate::prompt; + +pub struct CliUser<'a> { + db: &'a animals::AnimalDB, +} + +impl<'a> CliUser<'a> { + pub fn new(db: &'a animals::AnimalDB) -> Self { + CliUser { db } + } + + fn abort(&self) -> ! { + println!("Exit."); + std::process::exit(1); + } +} + +impl<'a> animals::UserAPI for CliUser<'_> { + fn notify_new_game(&self) { + println!("----- NEW GAME -----"); + } + + fn notify_game_ended(&self) { + println!("Game ended.\n"); + } + + fn notify_new_animal(&self, animal: &str) { + println!("Learned a new animal: {}", animal); + self.db.save("animals.txt").unwrap(); + } + + fn notify_victory(&self) { + println!("Yay, found an answer! Thanks for playing."); + } + + fn answer_yes_no(&self, question: &str) -> bool { + println!("{}", question); + match prompt::ask_yn("> ") { + None => self.abort(), + Some(b) => b, + } + } + + fn is_it_a(&self, animal: &str) -> bool { + println!("Is it a {}?", animal); + match prompt::ask_yn("> ") { + None => self.abort(), + Some(b) => b, + } + } + + fn what_is_it(&self) -> String { + println!("What animal is it?"); + match prompt::ask("> ") { + None => self.abort(), + Some(s) => s, + } + } + + fn how_to_tell_apart(&self, secret: &str, other: &str) -> (String, bool) { + println!("How to tell apart {} and {}?", secret, other); + let q: String = match prompt::ask("> ") { + None => self.abort(), + Some(s) => s, + }; + + println!("What is the answer for {}?", secret); + let b: bool = match prompt::ask_yn("> ") { + None => self.abort(), + Some(b) => b, + }; + + (q, b) + } +} diff --git a/src/main.rs b/src/main.rs index ec1c28b..fd31a63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,221 +1,19 @@ -use std::collections::HashMap; - mod animals; +mod prompt; mod slot; +mod testing; +mod cli_user; -struct AnswerMachine<'a> { - answer: String, - yes_no: HashMap<&'a str, bool>, - distinguish: HashMap<&'a str, (&'a str, bool)>, -} - -impl<'a> AnswerMachine<'a> { - fn abort(&self, msg: String) -> ! { - println!("{}", msg); - std::process::exit(1); - } -} - -impl<'a> animals::UserAPI for AnswerMachine<'a> { - fn notify_new_game(&self) { - println!("----- NEW GAME -----"); - println!("Secret animal is: {}", self.answer); - } - - fn notify_game_ended(&self) { - println!("Game ended.\n"); - } - - fn notify_new_animal(&self, animal: &str) { - println!("Learned a new animal: {}", animal); - } - - fn notify_victory(&self) { - println!("Yay, found an answer!"); - } - - fn answer_yes_no(&self, question: &str) -> bool { - println!("{}", question); - - let answer = match self.yes_no.get(question) { - Some(a) => *a, - None => { - self.abort(format!("* Missing answer for {}! *", &self.answer)); - } - }; - - println!("> {}", match answer { true => "yes", false => "no" }); - answer - } - - fn is_it_a(&self, animal: &str) -> bool { - println!("Is it a {}?", animal); - - let answer = animal == &self.answer; - println!("> {}", match answer { true => "yes", false => "no" }); - - answer - } - - fn what_is_it(&self) -> String { - println!("What animal is it?"); - println!("> {}", self.answer); - - self.answer.clone() - } - - fn how_to_tell_apart(&self, other: &str) -> (String, bool) { - println!("How to tell apart {} and {}?", self.answer, other); - - let (s, b) = self.distinguish.get(other).unwrap_or_else(|| { - self.abort(format!("* I don't know how to tell apart {} and {}! *", self.answer, other)); - }); - - println!("> {}", s); - println!("What is the answer for {}?", self.answer); - println!( - "> {}", - match *b { true => "yes", false => "no" } - ); - - (s.to_string(), *b) - } -} - -// a little hack for map initializer literals -macro_rules! map( - { $($key:expr => $value:expr),* } => { - { - let mut m = std::collections::HashMap::new(); - $( m.insert($key, $value); )* - m - } - }; -); +use crate::cli_user::CliUser; fn main() { - let db = animals::AnimalDB::new(); - - let duck = AnswerMachine { - answer: "duck".to_string(), - yes_no: map! { - "Does it quack?" => true - }, - distinguish: map! { - "cow" => ("Does it moo?", false) - }, - }; - db.start_game(&duck); + //testing::main(); - let dog = AnswerMachine { - answer: "dog".to_string(), - yes_no: map! { - "Does it quack?" => false, - "Does it woof?" => true, - "Is it wild?" => false - }, - distinguish: map! { - "duck" => ("Does it quack?", false), - "wolf" => ("Is it wild?", false) - }, - }; - db.start_game(&dog); - - let wolf = AnswerMachine { - answer: "wolf".to_string(), - yes_no: map! { - "Does it quack?" => false, - "Is it wild?" => true, - "Does it fly?" => false, - "Does it eat acorns?" => false - }, - distinguish: map! { - "dog" => ("Is it wild?", true) - }, - }; - db.start_game(&wolf); - - let cow = AnswerMachine { - answer: "cow".to_string(), - yes_no: map! { - "Does it quack?" => false, - "Is it wild?" => false, - "Does it woof?" => false, - "Does it give milk?" => true, - "Is it grown for meat?" => true - }, - distinguish: map! { - "dog" => ("Does it woof?", false) - }, - }; - db.start_game(&cow); - - let pig = AnswerMachine { - answer: "pig".to_string(), - yes_no: map! { - "Does it quack?" => false, - "Is it wild?" => false, - "Does it woof?" => false, - "Does it give milk?" => false - }, - distinguish: map! { - "cow" => ("Does it give milk?", false) - }, - }; - db.start_game(&pig); - - let goat = AnswerMachine { - answer: "goat".to_string(), - yes_no: map! { - "Does it quack?" => false, - "Is it wild?" => false, - "Does it woof?" => false, - "Does it give milk?" => true, - "Is it grown for meat?" => false - }, - distinguish: map! { - "cow" => ("Is it grown for meat?", false) - }, - }; - db.start_game(&goat); - - let raven = AnswerMachine { - answer: "raven".to_string(), - yes_no: map! { - "Does it quack?" => false, - "Is it wild?" => true, - "Does it woof?" => false, - "Does it fly?" => true - }, - distinguish: map! { - "wolf" => ("Does it fly?", true) - }, - }; - db.start_game(&raven); - - let boar = AnswerMachine { - answer: "boar".to_string(), - yes_no: map! { - "Does it quack?" => false, - "Is it wild?" => true, - "Does it woof?" => false, - "Does it fly?" => false - }, - distinguish: map! { - "wolf" => ("Does it eat acorns?", true) - }, - }; - db.start_game(&boar); - - - // try how the DB works with the learned animals - println!("===== Testing known animals ====="); + let db = animals::AnimalDB::new(); + db.load("animals.txt").unwrap_or(()); + let user = CliUser::new(&db); - db.start_game(&cow); - db.start_game(&dog); - db.start_game(&wolf); - db.start_game(&duck); - db.start_game(&pig); - db.start_game(&goat); - db.start_game(&raven); + loop { + db.start_game(&user); + } } diff --git a/src/prompt.rs b/src/prompt.rs new file mode 100644 index 0000000..def2588 --- /dev/null +++ b/src/prompt.rs @@ -0,0 +1,74 @@ +use std::io; +use std::io::Write; +use std::str::FromStr; + +fn flush() { + io::stdout().flush().expect("Failed to flush stdout"); +} + +struct YesNo(bool); +struct YesNoParseError(); + +impl From for bool { + fn from(yn: YesNo) -> Self { + yn.0 + } +} + +impl FromStr for YesNo { + type Err = YesNoParseError; + + fn from_str(s: &str) -> Result::Err> { + match s { + "true" => Ok(YesNo(true)), + "y" => Ok(YesNo(true)), + "1" => Ok(YesNo(true)), + "yes" => Ok(YesNo(true)), + "a" => Ok(YesNo(true)), + "false" => Ok(YesNo(false)), + "f" => Ok(YesNo(false)), + "no" => Ok(YesNo(false)), + "n" => Ok(YesNo(false)), + "0" => Ok(YesNo(false)), + _ => Err(YesNoParseError()), + } + } +} + +pub fn ask_yn(prompt: &str) -> Option { + match ask::(prompt) { + Some(yn) => Some(yn.into()), + None => None, + } +} + +pub fn ask(prompt: &str) -> Option +where + T: FromStr, +{ + let mut buf = String::new(); + loop { + print!("{}", prompt); + + flush(); + buf.clear(); + io::stdin() + .read_line(&mut buf) + .expect("Failed to read line"); + + let s = buf.trim(); + if s.is_empty() { + println!(); // empty string returns None as a signal to terminate the program + break None; + } + + match s.parse() { + Ok(val) => { + break Some(val); + } + Err(_) => { + println!("Could not parse \"{}\", please try again.", s); + } + }; + } +} diff --git a/src/testing.rs b/src/testing.rs new file mode 100644 index 0000000..ff60c27 --- /dev/null +++ b/src/testing.rs @@ -0,0 +1,239 @@ +use std::collections::HashMap; +use crate::animals; + +struct AnswerMachine<'a> { + answer: String, + yes_no: HashMap<&'a str, bool>, + distinguish: HashMap<&'a str, (&'a str, bool)>, +} + +impl<'a> AnswerMachine<'a> { + fn abort(&self, msg: String) -> ! { + println!("{}", msg); + std::process::exit(1); + } +} + +impl<'a> animals::UserAPI for AnswerMachine<'a> { + fn notify_new_game(&self) { + println!("----- NEW GAME -----"); + println!("Secret animal is: {}", self.answer); + } + + fn notify_game_ended(&self) { + println!("Game ended.\n"); + } + + fn notify_new_animal(&self, animal: &str) { + println!("Learned a new animal: {}", animal); + } + + fn notify_victory(&self) { + println!("Yay, found an answer!"); + } + + fn answer_yes_no(&self, question: &str) -> bool { + println!("{}", question); + + let answer = match self.yes_no.get(question) { + Some(a) => *a, + None => { + self.abort(format!("* Missing answer for {}! *", &self.answer)); + } + }; + + println!( + "> {}", + match answer { + true => "yes", + false => "no", + } + ); + answer + } + + fn is_it_a(&self, animal: &str) -> bool { + println!("Is it a {}?", animal); + + let answer = animal == &self.answer; + println!( + "> {}", + match answer { + true => "yes", + false => "no", + } + ); + + answer + } + + fn what_is_it(&self) -> String { + println!("What animal is it?"); + println!("> {}", self.answer); + + self.answer.clone() + } + + fn how_to_tell_apart(&self, secret: &str, other: &str) -> (String, bool) { + println!("How to tell apart {} and {}?", secret, other); + + let (s, b) = self.distinguish.get(other).unwrap_or_else(|| { + self.abort(format!( + "* I don't know how to tell apart {} and {}! *", + self.answer, other + )); + }); + + println!("> {}", s); + println!("What is the answer for {}?", self.answer); + println!( + "> {}", + match *b { + true => "yes", + false => "no", + } + ); + + (s.to_string(), *b) + } +} + +// a little hack for map initializer literals +macro_rules! map( + { $($key:expr => $value:expr),* } => { + { + let mut m = std::collections::HashMap::new(); + $( m.insert($key, $value); )* + m + } + }; +); + +#[allow(dead_code)] +pub fn main() { + let db = animals::AnimalDB::new(); + + let duck = AnswerMachine { + answer: "duck".to_string(), + yes_no: map! { + "Does it quack?" => true + }, + distinguish: map! { + "cow" => ("Does it moo?", false) + }, + }; + db.start_game(&duck); + + let dog = AnswerMachine { + answer: "dog".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Does it woof?" => true, + "Is it wild?" => false + }, + distinguish: map! { + "duck" => ("Does it quack?", false), + "wolf" => ("Is it wild?", false) + }, + }; + db.start_game(&dog); + + let wolf = AnswerMachine { + answer: "wolf".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => true, + "Does it fly?" => false, + "Does it eat acorns?" => false + }, + distinguish: map! { + "dog" => ("Is it wild?", true) + }, + }; + db.start_game(&wolf); + + let cow = AnswerMachine { + answer: "cow".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => false, + "Does it woof?" => false, + "Does it give milk?" => true, + "Is it grown for meat?" => true + }, + distinguish: map! { + "dog" => ("Does it woof?", false) + }, + }; + db.start_game(&cow); + + let pig = AnswerMachine { + answer: "pig".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => false, + "Does it woof?" => false, + "Does it give milk?" => false + }, + distinguish: map! { + "cow" => ("Does it give milk?", false) + }, + }; + db.start_game(&pig); + + let goat = AnswerMachine { + answer: "goat".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => false, + "Does it woof?" => false, + "Does it give milk?" => true, + "Is it grown for meat?" => false + }, + distinguish: map! { + "cow" => ("Is it grown for meat?", false) + }, + }; + db.start_game(&goat); + + let raven = AnswerMachine { + answer: "raven".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => true, + "Does it woof?" => false, + "Does it fly?" => true + }, + distinguish: map! { + "wolf" => ("Does it fly?", true) + }, + }; + db.start_game(&raven); + + let boar = AnswerMachine { + answer: "boar".to_string(), + yes_no: map! { + "Does it quack?" => false, + "Is it wild?" => true, + "Does it woof?" => false, + "Does it fly?" => false + }, + distinguish: map! { + "wolf" => ("Does it eat acorns?", true) + }, + }; + db.start_game(&boar); + + // try how the DB works with the learned animals + println!("===== Testing known animals ====="); + + db.start_game(&cow); + db.start_game(&dog); + db.start_game(&wolf); + db.start_game(&duck); + db.start_game(&pig); + db.start_game(&goat); + db.start_game(&raven); + + db.save("test_db.txt").unwrap(); +}