diff --git a/front_end/src/game_logic.rs b/front_end/src/game_logic.rs index 98ab872..e2c5111 100644 --- a/front_end/src/game_logic.rs +++ b/front_end/src/game_logic.rs @@ -14,8 +14,8 @@ pub fn check_win_draw(board: &BoardType) -> Option { } } - if has_empty_slot { - return None; + if !has_empty_slot { + return Some(BoardState::Empty); } let check_result = |state| -> Option { @@ -130,3 +130,200 @@ fn has_right_down_diagonal_at_idx(idx: usize, board: &BoardType) -> BoardState { } BoardState::Empty } + +#[cfg(test)] +mod tests { + use crate::state::{new_empty_board, BoardState}; + + use super::*; + + #[test] + fn test_horizontal_check() { + let board = new_empty_board(); + + for y in 0..(ROWS as usize) { + for x in 0..(COLS as usize) { + assert_eq!( + has_right_horizontal_at_idx(x + y * (COLS as usize), &board), + BoardState::Empty + ); + } + } + + board[50].replace(BoardState::Cyan); + board[51].replace(BoardState::Cyan); + board[52].replace(BoardState::Cyan); + board[53].replace(BoardState::Cyan); + + for y in 0..(ROWS as usize) { + for x in 0..(COLS as usize) { + let idx = x + y * (COLS as usize); + if idx == 50 { + assert_eq!(has_right_horizontal_at_idx(idx, &board), BoardState::Cyan); + } else { + assert_eq!(has_right_horizontal_at_idx(idx, &board), BoardState::Empty); + } + } + } + + board[51].replace(BoardState::Magenta); + + board[43].replace(BoardState::Magenta); + board[44].replace(BoardState::Magenta); + board[45].replace(BoardState::Magenta); + board[46].replace(BoardState::Magenta); + + for y in 0..(ROWS as usize) { + for x in 0..(COLS as usize) { + let idx = x + y * (COLS as usize); + if idx == 43 { + assert_eq!( + has_right_horizontal_at_idx(idx, &board), + BoardState::Magenta + ); + } else { + assert_eq!(has_right_horizontal_at_idx(idx, &board), BoardState::Empty); + } + } + } + } + + #[test] + fn test_vertical_check() { + let board = new_empty_board(); + + for y in 0..(ROWS as usize) { + for x in 0..(COLS as usize) { + assert_eq!( + has_down_vertical_at_idx(x + y * (COLS as usize), &board), + BoardState::Empty + ); + } + } + + board[30].replace(BoardState::Cyan); + board[37].replace(BoardState::Cyan); + board[44].replace(BoardState::Cyan); + board[51].replace(BoardState::Cyan); + + for y in 0..(ROWS as usize) { + for x in 0..(COLS as usize) { + let idx = x + y * (COLS as usize); + if idx == 30 { + assert_eq!(has_down_vertical_at_idx(idx, &board), BoardState::Cyan); + } else { + assert_eq!(has_down_vertical_at_idx(idx, &board), BoardState::Empty); + } + } + } + + board[16].replace(BoardState::Magenta); + board[23].replace(BoardState::Magenta); + board[30].replace(BoardState::Magenta); + board[37].replace(BoardState::Magenta); + + for y in 0..(ROWS as usize) { + for x in 0..(COLS as usize) { + let idx = x + y * (COLS as usize); + if idx == 16 { + assert_eq!(has_down_vertical_at_idx(idx, &board), BoardState::Magenta); + } else { + assert_eq!(has_down_vertical_at_idx(idx, &board), BoardState::Empty); + } + } + } + } + + #[test] + fn test_upper_diagonal_check() { + let board = new_empty_board(); + + board[44].replace(BoardState::Cyan); + board[38].replace(BoardState::Cyan); + board[32].replace(BoardState::Cyan); + board[26].replace(BoardState::Cyan); + + for y in 0..(ROWS as usize) { + for x in 0..(COLS as usize) { + let idx = x + y * (COLS as usize); + if idx == 44 { + assert_eq!(has_right_up_diagonal_at_idx(idx, &board), BoardState::Cyan); + } else { + assert_eq!(has_right_up_diagonal_at_idx(idx, &board), BoardState::Empty); + } + } + } + + board[38].replace(BoardState::Magenta); + + board[28].replace(BoardState::Magenta); + board[22].replace(BoardState::Magenta); + board[16].replace(BoardState::Magenta); + board[10].replace(BoardState::Magenta); + + for y in 0..(ROWS as usize) { + for x in 0..(COLS as usize) { + let idx = x + y * (COLS as usize); + if idx == 28 { + assert_eq!( + has_right_up_diagonal_at_idx(idx, &board), + BoardState::Magenta + ); + } else { + assert_eq!(has_right_up_diagonal_at_idx(idx, &board), BoardState::Empty); + } + } + } + } + + #[test] + fn test_lower_diagonal_check() { + let board = new_empty_board(); + + board[17].replace(BoardState::Cyan); + board[25].replace(BoardState::Cyan); + board[33].replace(BoardState::Cyan); + board[41].replace(BoardState::Cyan); + + for y in 0..(ROWS as usize) { + for x in 0..(COLS as usize) { + let idx = x + y * (COLS as usize); + if idx == 17 { + assert_eq!( + has_right_down_diagonal_at_idx(idx, &board), + BoardState::Cyan + ); + } else { + assert_eq!( + has_right_down_diagonal_at_idx(idx, &board), + BoardState::Empty + ); + } + } + } + + board[25].replace(BoardState::Magenta); + + board[28].replace(BoardState::Magenta); + board[36].replace(BoardState::Magenta); + board[44].replace(BoardState::Magenta); + board[52].replace(BoardState::Magenta); + + for y in 0..(ROWS as usize) { + for x in 0..(COLS as usize) { + let idx = x + y * (COLS as usize); + if idx == 28 { + assert_eq!( + has_right_down_diagonal_at_idx(idx, &board), + BoardState::Magenta + ); + } else { + assert_eq!( + has_right_down_diagonal_at_idx(idx, &board), + BoardState::Empty + ); + } + } + } + } +} diff --git a/front_end/src/html_helper.rs b/front_end/src/html_helper.rs new file mode 100644 index 0000000..6e4efdf --- /dev/null +++ b/front_end/src/html_helper.rs @@ -0,0 +1,54 @@ +use web_sys::{window, Document, Window}; + +pub fn get_window_document() -> Result<(Window, Document), String> { + let window = window().ok_or_else(|| String::from("Failed to get window"))?; + let document = window + .document() + .ok_or_else(|| String::from("Failed to get document"))?; + + Ok((window, document)) +} + +pub fn append_to_info_text( + document: &Document, + id: &str, + msg: &str, + limit: u32, +) -> Result<(), String> { + let info_text = document + .get_element_by_id(id) + .ok_or_else(|| format!("Failed to get info_text \"{}\"", id))?; + + let height = info_text.client_height(); + + // create the new text to be appended in the text + let p = document + .create_element("p") + .map_err(|e| format!("{:?}", e))?; + + p.set_inner_html(msg); + + // check if scrolled to top + let at_top: bool = info_text.scroll_top() <= height - info_text.scroll_height(); + + // append text to output + info_text + .append_with_node_1(&p) + .map_err(|e| format!("{:?}", e))?; + + while info_text.child_element_count() > limit { + info_text + .remove_child( + &info_text.first_child().ok_or_else(|| { + format!("Failed to get first_child() of info_text \"{}\"", id) + })?, + ) + .map_err(|e| format!("{:?}", e))?; + } + + if at_top { + info_text.set_scroll_top(height - info_text.scroll_height()); + } + + Ok(()) +} diff --git a/front_end/src/main.rs b/front_end/src/main.rs index eeca10f..a200b4e 100644 --- a/front_end/src/main.rs +++ b/front_end/src/main.rs @@ -1,6 +1,7 @@ mod ai; mod constants; mod game_logic; +mod html_helper; mod random_helper; mod state; mod yew_components; diff --git a/front_end/src/state.rs b/front_end/src/state.rs index 8b8e2cc..5c571e6 100644 --- a/front_end/src/state.rs +++ b/front_end/src/state.rs @@ -9,7 +9,7 @@ pub enum GameState { SinglePlayer, LocalMultiplayer, NetworkedMultiplayer, - PostGameResults(Turn), + PostGameResults(BoardState), } impl Default for GameState { @@ -108,6 +108,67 @@ impl Turn { pub type BoardType = [Rc>; 56]; +pub fn new_empty_board() -> BoardType { + [ + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + Rc::new(Cell::new(BoardState::default())), + ] +} + #[derive(Clone, Debug, PartialEq)] pub struct SharedState { pub board: BoardType, @@ -119,64 +180,7 @@ impl Default for SharedState { fn default() -> Self { Self { // cannot use [; 56] because Rc does not impl Copy - board: [ - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - Rc::new(Cell::new(BoardState::default())), - ], + board: new_empty_board(), game_state: Rc::new(Cell::new(GameState::default())), turn: Rc::new(Cell::new(Turn::CyanPlayer)), } diff --git a/front_end/src/yew_components.rs b/front_end/src/yew_components.rs index 56f6e84..b6fe872 100644 --- a/front_end/src/yew_components.rs +++ b/front_end/src/yew_components.rs @@ -1,7 +1,11 @@ use crate::constants::{COLS, INFO_TEXT_MAX_ITEMS, ROWS}; +use crate::game_logic::check_win_draw; +use crate::html_helper::{append_to_info_text, get_window_document}; use crate::state::{BoardState, GameState, SharedState, Turn}; + use std::cell::Cell; use std::rc::Rc; + use yew::prelude::*; pub struct MainMenu {} @@ -117,6 +121,13 @@ impl Component for Slot { .context::(Callback::noop()) .expect("state to be set"); + match shared.game_state.get() { + GameState::MainMenu => return false, + GameState::SinglePlayer + | GameState::LocalMultiplayer + | GameState::NetworkedMultiplayer => (), + GameState::PostGameResults(_) => return false, + } if shared.game_state.get() == GameState::MainMenu { return false; } @@ -228,8 +239,8 @@ impl Component for Wrapper { .link() .context::(Callback::noop()) .expect("state to be set"); - let window = web_sys::window().expect("no window exists"); - let document = window.document().expect("window should have a document"); + let (window, document) = + get_window_document().expect("Should be able to get Window and Document"); match msg { WrapperMsg::Pressed(idx) => { @@ -268,99 +279,82 @@ impl Component for Wrapper { placed = true; } - // DEBUG - //log::info!("{} is {:?}", idx, shared.board[idx as usize].get()); - - // DEBUG - //log::info!("{}", &output_str); - - // info text below the grid - if let Some(info_text) = document.get_element_by_id("info_text0") { - let height = info_text.client_height(); - - // create the new text to be appended in the output - let p = document - .create_element("p") - .expect("document should be able to create

"); - let output_str = match placed { - true => format!("{} placed into slot {}", current_player, bottom_idx), - false => "Invalid place to insert".into(), - }; - p.set_text_content(Some(&output_str)); - - // DEBUG - //log::info!( - // "pre: scroll top is {}, scroll height is {}", - // info_text.scroll_top(), - // info_text.scroll_height() - //); - - // check if scrolled to top - let at_top: bool = info_text.scroll_top() <= height - info_text.scroll_height(); - - // append text to output - info_text - .append_with_node_1(&p) - .expect("should be able to append to info_text"); - while info_text.child_element_count() > INFO_TEXT_MAX_ITEMS { - info_text - .remove_child(&info_text.first_child().unwrap()) - .expect("should be able to limit items in info_text"); + // check for win + let check_win_draw_opt = check_win_draw(&shared.board); + if let Some(endgame_state) = check_win_draw_opt { + if endgame_state == BoardState::Empty { + // draw + let text_append_result = append_to_info_text( + &document, + "info_text0", + "Game ended in a draw", + INFO_TEXT_MAX_ITEMS, + ); + if let Err(e) = text_append_result { + log::warn!("ERROR: text append to info_text0 failed: {}", e); + } + } else { + // a player won + let turn = Turn::from(endgame_state); + let text_string = + format!("{} has won", turn.get_color(), turn); + let text_append_result = append_to_info_text( + &document, + "info_text0", + &text_string, + INFO_TEXT_MAX_ITEMS, + ); + if let Err(e) = text_append_result { + log::warn!("ERROR: text append to info_text0 failed: {}", e); + } } - // DEBUG - //log::info!("at_top is {}", if at_top { "true" } else { "false" }); - - // scroll to top only if at top - if at_top { - info_text.set_scroll_top(height - info_text.scroll_height()); + let text_append_result = + append_to_info_text(&document, "info_text1", "Game Over", 1); + if let Err(e) = text_append_result { + log::warn!("ERROR: text append to info_text1 failed: {}", e); } - // DEBUG - //log::info!( - // "post: scroll top is {}, scroll height is {}", - // info_text.scroll_top(), - // info_text.scroll_height() - //); + shared + .game_state + .replace(GameState::PostGameResults(endgame_state)); } else { - log::warn!("Failed to get bottom \"info_text\""); - } + // game is still ongoing - // info text right of the grid - if let Some(info_text) = document.get_element_by_id("info_text1") { - let height = info_text.client_height(); + // info text below the grid + { + let output_str = match placed { + true => format!("{} placed into slot {}", current_player, bottom_idx), + false => "Invalid place to insert".into(), + }; - // create the new text to be appended in the output - let p = document - .create_element("p") - .expect("document should be able to create

"); - let turn = shared.turn.get(); - p.set_inner_html(&format!( - "It is {}'s turn", - turn.get_color(), - turn - )); - - // check if scrolled to top - let at_top: bool = info_text.scroll_top() <= height - info_text.scroll_height(); - - // append text to output - info_text - .append_with_node_1(&p) - .expect("should be able to append to info_text"); - while info_text.child_element_count() > 1 { - info_text - .remove_child(&info_text.first_child().unwrap()) - .expect("should be able to limit items in info_text"); + let text_append_result = append_to_info_text( + &document, + "info_text0", + &output_str, + INFO_TEXT_MAX_ITEMS, + ); + if let Err(e) = text_append_result { + log::warn!("ERROR: text append to info_text0 failed: {}", e); + } } - // scroll to top only if at top - if at_top { - info_text.set_scroll_top(height - info_text.scroll_height()); + // info text right of the grid + { + let turn = shared.turn.get(); + let output_str = format!( + "It is {}'s turn", + turn.get_color(), + turn + ); + + let text_append_result = + append_to_info_text(&document, "info_text1", &output_str, 1); + if let Err(e) = text_append_result { + log::warn!("ERROR: text append to info_text1 failed: {}", e); + } } - } else { - log::warn!("Failed to get side \"info_text\""); - } + } // else: game is still ongoing after logic check } // WrapperMsg::Pressed(idx) => } // match (msg)