cleaning, added CLI UI, added file load/save

master
Ondřej Hruška 5 years ago
parent bcfc006259
commit 7b364c6f6f
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 21
      animals.txt
  2. 253
      src/animals.rs
  3. 76
      src/cli_user.rs
  4. 224
      src/main.rs
  5. 74
      src/prompt.rs
  6. 239
      src/testing.rs

@ -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

@ -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<i32, AnimalStorageEntry>,
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<AnimalStorage> {
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<NodeStruct>,
}
pub struct AnimalDB {
root: Slot<NodeStruct>,
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<NodeStruct>,
}
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<NodeStruct>,
) {
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,

@ -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)
}
}

@ -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);
}
}

@ -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<YesNo> for bool {
fn from(yn: YesNo) -> Self {
yn.0
}
}
impl FromStr for YesNo {
type Err = YesNoParseError;
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::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<bool> {
match ask::<YesNo>(prompt) {
Some(yn) => Some(yn.into()),
None => None,
}
}
pub fn ask<T>(prompt: &str) -> Option<T>
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);
}
};
}
}

@ -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();
}
Loading…
Cancel
Save