mod utils;
use std::{
collections::HashMap,
convert::TryFrom,
ops::{Deref, DerefMut},
};
use wasm_bindgen::prelude::*;
#[derive(Debug)]
#[wasm_bindgen]
pub enum HangmanError {
InvalidLetter,
AlreadyGuessed,
GameOver,
}
impl From<HangmanError> for JsValue {
fn from(he: HangmanError) -> Self {
JsValue::from(match he {
HangmanError::InvalidLetter => "Invalid Letter!",
HangmanError::AlreadyGuessed => "Already Guessed!",
HangmanError::GameOver => "Game is over!",
})
}
}
#[wasm_bindgen]
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct InternalLetter(u8);
impl AsRef<u8> for InternalLetter {
fn as_ref(&self) -> &u8 {
&self.0
}
}
impl From<InternalLetter> for u8 {
fn from(l: InternalLetter) -> Self {
l.0
}
}
impl From<InternalLetter> for String {
fn from(l: InternalLetter) -> Self {
let u: u8 = l.into();
let s: String = std::str::from_utf8(&[u])
.expect("InternalLetters are always valid utf-8")
.to_string();
s
}
}
impl TryFrom<char> for InternalLetter {
type Error = HangmanError;
fn try_from(value: char) -> Result<Self, Self::Error> {
let mut buf: [u8; 4] = [0; 4];
if value.is_ascii_alphabetic() || value == '_' {
let value = value.to_ascii_uppercase();
value.encode_utf8(&mut buf);
let byte: u8 = buf[0];
assert_eq!(value, byte as char);
Ok(InternalLetter(byte))
} else {
Err(HangmanError::InvalidLetter)
}
}
}
impl From<InternalLetter> for char {
fn from(l: InternalLetter) -> Self {
l.0 as char
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
#[wasm_bindgen]
pub enum GameState {
Won,
Lost,
InProgress,
}
#[wasm_bindgen(inspectable, readonly)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum LetterGuessedState {
CorrectGuess,
Unguessed,
}
#[wasm_bindgen(readonly, inspectable)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum GuessResult {
Correct,
Incorrect,
}
fn string_repr<T>(source: &Vec<T>, char_mapper: impl FnMut(&T) -> u8) -> String {
let entries: Vec<u8> = source.iter().map(char_mapper).collect();
let entries = entries.as_slice();
std::str::from_utf8(entries)
.expect("All chars are valid UTF-8")
.to_string()
}
#[derive(PartialEq, Eq)]
#[wasm_bindgen(inspectable)]
pub struct WordLetter {
#[wasm_bindgen(readonly)]
pub letter: InternalLetter,
#[wasm_bindgen(readonly)]
pub state: LetterGuessedState,
}
impl WordLetter {
pub fn guessed(&self) -> bool {
self.state == LetterGuessedState::CorrectGuess
}
}
#[wasm_bindgen(inspectable)]
pub struct GuessLetter {
#[wasm_bindgen(readonly)]
pub letter: InternalLetter,
#[wasm_bindgen(readonly)]
pub state: GuessResult,
}
struct WordList {}
impl WordList {
pub fn select_random_word() -> Word {
Word(
"EGGPLANT"
.chars()
.map(|c| {
let letter =
InternalLetter::try_from(c).expect("WordList always uses valid chars");
WordLetter {
letter,
state: LetterGuessedState::Unguessed,
}
})
.collect(),
)
}
}
#[wasm_bindgen]
pub struct Word(Vec<WordLetter>);
impl Deref for Word {
type Target = Vec<WordLetter>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Word {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Word {
pub fn guess(&mut self, guess: &InternalLetter) -> GuessResult {
let mut guess_result: GuessResult = GuessResult::Incorrect;
self.iter_mut().for_each(|f| {
if f.letter == *guess {
f.state = LetterGuessedState::CorrectGuess;
guess_result = GuessResult::Correct;
}
});
guess_result
}
pub fn completely_guessed(&self) -> bool {
self.0.iter().all(WordLetter::guessed)
}
}
#[wasm_bindgen(inspectable)]
pub struct Game {
#[wasm_bindgen(skip)]
pub guesses_left: u8,
current_word: Word,
guesses: HashMap<InternalLetter, GuessResult>,
}
#[wasm_bindgen]
impl Game {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
guesses_left: 6,
guesses: HashMap::with_capacity(15),
current_word: WordList::select_random_word(),
}
}
#[wasm_bindgen(getter)]
pub fn guesses_left(&self) -> u8 {
self.guesses_left
}
#[wasm_bindgen(getter)]
#[inline]
pub fn state(&self) -> GameState {
if self.guesses_left == 0 {
GameState::Lost
} else if self.current_word.completely_guessed() {
GameState::Won
} else {
GameState::InProgress
}
}
#[wasm_bindgen(getter)]
pub fn current_word(&self) -> String {
match self.state() {
GameState::InProgress => Self::to_in_progress(&self.current_word),
_ => Self::to_game_end(&self.current_word),
}
}
#[wasm_bindgen]
pub fn guess_letter(&mut self, letter: char) -> Result<GameState, HangmanError> {
let guess = InternalLetter::try_from(letter).or(Err(HangmanError::InvalidLetter))?;
if self.guesses.get(&guess).is_some() {
return Err(HangmanError::AlreadyGuessed);
} else if self.state() != GameState::InProgress {
return Err(HangmanError::GameOver);
};
let result = self.current_word.guess(&guess);
match result {
GuessResult::Incorrect => {
self.guesses_left -= 1;
}
GuessResult::Correct => {}
}
Ok(self.state())
}
fn to_in_progress(letters: &Vec<WordLetter>) -> String {
let mapper = |l: &WordLetter| match l {
WordLetter {
letter,
state: LetterGuessedState::CorrectGuess,
} => (u8::from(*letter)),
WordLetter {
state: LetterGuessedState::Unguessed,
..
} => b"_"[0],
};
string_repr(letters, mapper)
}
fn to_game_end(letters: &Vec<WordLetter>) -> String {
let mapper = |l: &WordLetter| l.letter.into();
string_repr(letters, mapper)
}
#[wasm_bindgen(getter)]
pub fn correct_guesses(&self) -> Box<[JsValue]> {
let correct: Vec<JsValue> = self
.guesses
.iter()
.filter_map(|(letter, guess)| {
if let GuessResult::Correct = guess {
let s: String = (*letter).into();
let j: JsValue = (*s).into();
Some(j)
} else {
None
}
})
.collect();
correct.into_boxed_slice()
}
#[wasm_bindgen(getter, skip_typescript)]
pub fn incorrect_guesses(&self) -> Box<[JsValue]> {
let incorrect: Vec<JsValue> = self
.guesses
.iter()
.filter_map(|(letter, guess)| {
if let GuessResult::Incorrect = guess {
let s: String = (*letter).into();
let j: JsValue = (*s).into();
Some(j)
} else {
None
}
})
.collect();
incorrect.into_boxed_slice()
}
}
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, hangman!");
}
#[cfg(test)]
mod tests {
use std::convert::TryInto;
use super::*;
#[test]
fn internal_letter_works_for_letters_and_underscore() -> Result<(), HangmanError> {
let lower: char = 'a';
let upper: char = 'Z';
let under: char = '_';
let lower_r: InternalLetter = lower.try_into()?;
assert_eq!("A".to_string(), String::from(lower_r));
let upper_r: InternalLetter = upper.try_into()?;
assert_eq!("Z".to_string(), String::from(upper_r));
let under_r: InternalLetter = under.try_into()?;
assert_eq!("_".to_string(), String::from(under_r));
Ok(())
}
#[test]
fn internal_letter_does_not_work_for_digits() {
let digit = '9';
assert!(InternalLetter::try_from(digit).is_err());
}
}