281 lines
7.4 KiB
Rust
281 lines
7.4 KiB
Rust
#![allow(unused_macros)]
|
|
|
|
use indexmap::IndexMap;
|
|
use std::panic;
|
|
use sycamore::prelude::*;
|
|
use wasm_bindgen::prelude::*;
|
|
use web_sys::Event;
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
struct Question {
|
|
prompt: &'static str,
|
|
answer: char,
|
|
choices: IndexMap<char, &'static str>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum AppMode {
|
|
Quiz,
|
|
Endgame,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct Props {
|
|
mode: Signal<AppMode>,
|
|
errors: Signal<usize>,
|
|
}
|
|
|
|
macro_rules! indexmap {
|
|
($($key:expr => $value:expr),*) => {{
|
|
let mut map = IndexMap::with_capacity(5);
|
|
|
|
$(
|
|
map.insert($key, $value);
|
|
)*
|
|
|
|
map
|
|
}}
|
|
}
|
|
|
|
macro_rules! wasm_import {
|
|
($name:ident()) => {
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
#[wasm_bindgen]
|
|
pub fn $name();
|
|
}
|
|
};
|
|
($name:ident( $( $arg:ident: $type:ty ),* )) => {
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
#[wasm_bindgen]
|
|
pub fn $name($($arg: $type),*);
|
|
}
|
|
};
|
|
($name:ident($($arg:ident: $type:ty),*) > $ret:ty) => {
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
#[wasm_bindgen]
|
|
pub fn $name($($arg: $type)*) -> $ret;
|
|
}
|
|
};
|
|
($name:ident() > $ret:ty) => {
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
#[wasm_bindgen]
|
|
pub fn $name() -> $ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
macro_rules! wasm_import_type {
|
|
($name:ident) => {
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
pub type $name;
|
|
}
|
|
};
|
|
($($name:ident),*) => {
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
$(pub type $name;)*
|
|
}
|
|
}
|
|
}
|
|
|
|
macro_rules! read_js_value {
|
|
($target:expr, $key:expr) => {
|
|
js_sys::Reflect::get(&$target, &wasm_bindgen::JsValue::from_str($key))
|
|
};
|
|
}
|
|
|
|
macro_rules! create_question {
|
|
($prompt:expr, $answer:expr, $choices:expr) => {
|
|
Question {
|
|
prompt: $prompt,
|
|
answer: $answer,
|
|
choices: $choices,
|
|
}
|
|
};
|
|
}
|
|
|
|
wasm_import!(log(s: String));
|
|
|
|
#[component(QuizComponent<G>)]
|
|
fn quiz_component(props: Props) -> View<G> {
|
|
let questions = [
|
|
create_question!(
|
|
"Which of these fruits contains potassium?",
|
|
'B',
|
|
indexmap! {
|
|
'A' => "apple",
|
|
'B' => "banana",
|
|
'C' => "cherry",
|
|
'D' => "dragonfruit"
|
|
}
|
|
),
|
|
create_question!(
|
|
"The moon's light actually comes from the sun",
|
|
'A',
|
|
indexmap! {
|
|
'A' => "True",
|
|
'B' => "False"
|
|
}
|
|
),
|
|
create_question!(
|
|
"Which of these countries is not considered a kingdom?",
|
|
'C',
|
|
indexmap! {
|
|
'A' => "Belgium",
|
|
'B' => "Denmark",
|
|
'C' => "Monaco",
|
|
'D' => "Sweden"
|
|
}
|
|
),
|
|
create_question!(
|
|
"Force is measured in which unit?",
|
|
'B',
|
|
indexmap! {
|
|
'A' => "Kilograms",
|
|
'B' => "Newtons",
|
|
'C' => "Joules"
|
|
}
|
|
),
|
|
create_question!(
|
|
"What does a vulcanologist study?",
|
|
'A',
|
|
indexmap! {
|
|
'A' => "Volcanoes",
|
|
'B' => "Plants",
|
|
'C' => "Constellations"
|
|
}
|
|
),
|
|
create_question!(
|
|
"Energy is measured in which unit?",
|
|
'C',
|
|
indexmap! {
|
|
'A' => "Kilograms",
|
|
'B' => "Newtons",
|
|
'C' => "Joules"
|
|
}
|
|
),
|
|
];
|
|
let index = Signal::new(0usize);
|
|
let current_prompt = create_memo(cloned!((index, questions) => move || {
|
|
questions[*index.get()].prompt.to_string()
|
|
}));
|
|
let current_choices = create_memo(cloned!((index, questions) => move || {
|
|
questions[*index.get()].choices.clone().into_iter().collect::<Vec<(char, &str)>>()
|
|
}));
|
|
|
|
let correct = Signal::new(true);
|
|
let errors = props.errors;
|
|
let mode = props.mode;
|
|
|
|
let answer_question = cloned!((index, questions, correct, errors) => move |e: Event| {
|
|
let answer = questions[*index.get()].answer;
|
|
let id = read_js_value!(e.target().unwrap(), "id").unwrap().as_string().unwrap().chars().collect::<Vec<char>>()[0];
|
|
if answer == id {
|
|
let current_index = *index.get();
|
|
if current_index == 5 {
|
|
mode.set(AppMode::Endgame);
|
|
} else {
|
|
index.set(*index.get() + 1);
|
|
correct.set(true);
|
|
}
|
|
} else {
|
|
errors.set(*errors.get() + 1);
|
|
correct.set(false);
|
|
}
|
|
});
|
|
|
|
view! {
|
|
div(class="card") {
|
|
p(class="text-align-center") { (current_prompt.get()) }
|
|
div(class="text-align-center") {
|
|
Keyed(KeyedProps {
|
|
iterable: current_choices,
|
|
template: move |(c, s)| view! {
|
|
button(class="fw", id=c, on:click=answer_question.clone()) { (c)") "(s) }
|
|
},
|
|
key: |(_, s)| *s
|
|
})
|
|
}
|
|
}
|
|
(if !*correct.get() {
|
|
view! {
|
|
p(class="text-align-center", style="color: red") { "Incorrect. Please try again." }
|
|
}
|
|
} else {
|
|
view! {}
|
|
})
|
|
}
|
|
}
|
|
|
|
#[component(EndGameComponent<G>)]
|
|
fn end_game_component(props: Props) -> View<G> {
|
|
let mode = props.mode;
|
|
let errors = props.errors;
|
|
|
|
let (failed, grade) = cloned!(errors => {
|
|
let num_errors = (*errors.get()) as f64;
|
|
|
|
let mut percentage = num_errors / 6f64;
|
|
percentage *= 100f64;
|
|
|
|
(percentage > 70f64, format!("{:.2}%", 100f64 - percentage))
|
|
});
|
|
|
|
let restart = cloned!((mode, errors) => move |_| {
|
|
errors.set(0);
|
|
mode.set(AppMode::Quiz);
|
|
});
|
|
|
|
view! {
|
|
(if failed {
|
|
view! {
|
|
p(class="text-align-center") { "Sorry, you failed. Better luck next time! Here's how you did:" }
|
|
}
|
|
} else {
|
|
view! {
|
|
p(class="text-align-center") { "Great job, you passed! Here's how you did:" }
|
|
}
|
|
})
|
|
br
|
|
div(class="text-align-center") {
|
|
div(class="card text-align-center inline", style="color: red") {
|
|
p {"Errors:"}
|
|
p { (errors.get()) }
|
|
}
|
|
div(class="card text-align-center inline") {
|
|
p {"Grade:"}
|
|
p { (grade) }
|
|
}
|
|
br
|
|
button(on:click=restart) { "Restart" }
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
panic::set_hook(Box::new(console_error_panic_hook::hook));
|
|
let mode = Signal::new(AppMode::Endgame);
|
|
let errors = Signal::new(0usize);
|
|
|
|
create_effect(cloned!(errors => move || {
|
|
log(format!("{}", errors.get()));
|
|
}));
|
|
|
|
sycamore::render(|| {
|
|
view! {
|
|
div(class="wrapper") {
|
|
h1(class="text-align-center") { "RiddlRS" }
|
|
(match *mode.get() {
|
|
AppMode::Quiz => view! { QuizComponent(Props { mode: cloned!(mode => mode), errors: cloned!(errors => errors) }) },
|
|
_ => view! { EndGameComponent(Props { mode: cloned!(mode => mode), errors: cloned!(errors => errors) }) }
|
|
})
|
|
}
|
|
}
|
|
});
|
|
}
|