Skip to content

Instantly share code, notes, and snippets.

@AndrewJakubowicz
Created May 22, 2018 17:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save AndrewJakubowicz/a4c6eb5f838618080f31636efde29d8f to your computer and use it in GitHub Desktop.
Save AndrewJakubowicz/a4c6eb5f838618080f31636efde29d8f to your computer and use it in GitHub Desktop.
Rust Quiz Game
[package]
name = "quiz"
version = "0.1.0"
authors = ["andrew <spyr1014@gmail.com>"]
[dependencies]
//! Inspired by [Gophercises Exercise 1](https://gophercises.com/)
//! Author: Andrew Jakubowicz (YouCodeThings)
//!
//! [Youtube video, explaining this code.](https://www.youtube.com/watch?v=bR3A_0IMSuU)
//! I've over documented this code so that it's easier to explore.
//! Without documentation the code is around 60 lines.
//!
//! The main feature of this quiz game is being able to cancel a user input.
//! We do this by using message passing.
//!
//! Let's quickly look at what a normal user prompt code might look like.
//!
//! ```
//! # In python it might look like this
//! user_input = input("prompt> ")
//! ```
//!
//! In the code above, the execution of the code freezes to wait for the input.
//! To be able to cancel this prompt, we need to be able to run code.
//! This is why we run the code in a separate thread.
//! This will make only the separate thread freeze. Now we can use the mpsc channel
//! between the threads to timeout if the user takes too long.
//!
// imports. We need `fs` or file system module.
use std::fs;
// `self` allows us to reference the `io` module later.
// Read and Write trait let us read the quiz file and write to `stdout`.
use std::io::{self, Read, Write};
// `mpsc` stands for multiple producer and single consumer.
// This is our channel between threads.
use std::sync::mpsc;
// import the thread module.
use std::thread;
// Duration is how we time the user.
use std::time::Duration;
// This is the entry to the program. In a binary,
// the `main` function is always run first.
fn main() {
let quiz_tsv_filename = "quiz.tsv";
// Time the user is given for each question.
let timeout = 4;
// Open and read the contents from the quiz file.
// This requires 3 steps.
// First we open the file. Then create a new string buffer.
// Finally we read the contents of the file into the string variable.
let mut quiz_file = fs::File::open(quiz_tsv_filename).unwrap();
let mut buf = String::new();
quiz_file.read_to_string(&mut buf).unwrap();
// We can count the number of questions by counting the lines.
// The type signature of `lines` takes an immutable borrow or `&self`,
// so doesn't consume the string. This is how we can use the string buffer twice.
let total_questions = buf.lines().count();
// Here we use iterator chaining to break up the logic.
// Essentially we can imagine each line in the iterator flowing through the
// various iterator methods.
// `lines()` creates an iterator over each line in the string buffer.
let score = buf.lines()
// the `map` method transforms each line from |String| -> (String, String) in this case.
.map(|line| {
let mut q_a = line.split('\t').map(|s| s.to_string());
let question = q_a.next().expect("No question found.");
let answer = q_a.next().expect("No answer found.");
(question, answer)
})
// This map transforms from the tuple to a Option<bool>.
// Although we could combine the two maps together, I like to separate tasks.
// It makes the code easier to manipulate.
// None represents the user failing the timout, otherwise we have Some bool returned.
.map(|(question, answer)| test_question(&question, &answer, timeout))
// `take_while` continues the iterator while the condition is true.
// Here we stop the iterator when the user fails to answer in the time limit,
// because the option type will not be `Some`.
.take_while(|o| o.is_some())
// We have an invariant that any value below the `take_while` must be Some,
// so we can unwrap.
// (You should still assume coder error could happen so `unwrap` is probably a bad idea)
.map(|o| o.unwrap())
// Now we want to count the correct answers by filtering out the false bool.
.filter(|p| *p)
// `count` will count the number of elements that reach it.
// In this case we are counting the number of true values or correct answers to the questions.
.count();
println!("Score: {} / {}", score, total_questions);
}
/// Tests the user with a timeout.
fn test_question(question: &str, answer: &str, timeout: u32) -> Option<bool> {
print!("{}", question);
// We have to flush the question to the display, otherwise
// it may not appear because Rust is trying to optimize display calls.
io::stdout().flush().expect("Failed to flush the buffer");
// Set up our transmitter and receiver to use between threads.
let (transmitter, receiver) = mpsc::channel();
// Spawn a thread with the user input code.
thread::spawn(move || {
// Read the user input into a buffer.
let mut buffer = String::new();
io::stdin()
.read_line(&mut buffer)
.expect("Failed to read user input");
let buffer = buffer.trim().to_string();
// And send the buffer into the transmitter.
// This is how we get the buffer out of the thread.
transmitter.send(buffer).expect("Failed to send user input");
});
// The spawned thread doesn't block so we jump straight to this line of code.
// Here we want the receiver to wait for the given time or until the users
// answer is provided.
// GOD METHOD CHAINING IS NICE.
receiver
.recv_timeout(Duration::new(timeout as u64, 0))
.or_else(|o| {
// If there is an error we print this and re-wrap the error.
// Hack to use an error as a side effect.
println!("\nYou ran out of time! :(");
Err(o)
})
// `ok` changes `Result<A,B>` into `Option<A>`.
.ok()
// Now we can transform the Option<String> to an Option<bool> using a mapping function.
.map(|buffer| buffer == answer)
// At this stage the function can return 3 things.
// Some(true) if the answer was correct.
// Some(false) if the answer was wrong.
// None if the timeout triggered.
}
2+2= 4
What is my name? youcodethings
Ready to see how to read a file? yes
Ready to see how to cancel waiting for a user input using threads? yes
Are you going to like and subscribe? yes
Proof of the timeout. yes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment