Also added unit tests for win/draw checks.
}
}
- if has_empty_slot {
- return None;
+ if !has_empty_slot {
+ return Some(BoardState::Empty);
}
let check_result = |state| -> Option<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
+ );
+ }
+ }
+ }
+ }
+}
--- /dev/null
+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(())
+}
mod ai;
mod constants;
mod game_logic;
+mod html_helper;
mod random_helper;
mod state;
mod yew_components;
SinglePlayer,
LocalMultiplayer,
NetworkedMultiplayer,
- PostGameResults(Turn),
+ PostGameResults(BoardState),
}
impl Default for GameState {
pub type BoardType = [Rc<Cell<BoardState>>; 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,
fn default() -> Self {
Self {
// cannot use [<type>; 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)),
}
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 {}
.context::<SharedState>(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;
}
.link()
.context::<SharedState>(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) => {
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 <p>");
- 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!("<b class=\"{}\">{} has won</b>", 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", "<b>Game Over</b>", 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();
-
- // create the new text to be appended in the output
- let p = document
- .create_element("p")
- .expect("document should be able to create <p>");
- let turn = shared.turn.get();
- p.set_inner_html(&format!(
- "<b class=\"{}\">It is {}'s turn</b>",
- 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");
+ // 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(),
+ };
+
+ 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!(
+ "<b class=\"{}\">It is {}'s turn</b>",
+ 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)