Impl use of win/draw check, fixes

Also added unit tests for win/draw checks.
This commit is contained in:
Stephen Seo 2022-03-09 17:29:53 +09:00
parent 9e9bb0758c
commit b902b1c7b4
5 changed files with 396 additions and 146 deletions

View file

@ -14,8 +14,8 @@ pub fn check_win_draw(board: &BoardType) -> Option<BoardState> {
} }
} }
if has_empty_slot { if !has_empty_slot {
return None; return Some(BoardState::Empty);
} }
let check_result = |state| -> Option<BoardState> { let check_result = |state| -> Option<BoardState> {
@ -130,3 +130,200 @@ fn has_right_down_diagonal_at_idx(idx: usize, board: &BoardType) -> BoardState {
} }
BoardState::Empty 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
);
}
}
}
}
}

View file

@ -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(())
}

View file

@ -1,6 +1,7 @@
mod ai; mod ai;
mod constants; mod constants;
mod game_logic; mod game_logic;
mod html_helper;
mod random_helper; mod random_helper;
mod state; mod state;
mod yew_components; mod yew_components;

View file

@ -9,7 +9,7 @@ pub enum GameState {
SinglePlayer, SinglePlayer,
LocalMultiplayer, LocalMultiplayer,
NetworkedMultiplayer, NetworkedMultiplayer,
PostGameResults(Turn), PostGameResults(BoardState),
} }
impl Default for GameState { impl Default for GameState {
@ -108,6 +108,67 @@ impl Turn {
pub type BoardType = [Rc<Cell<BoardState>>; 56]; 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)] #[derive(Clone, Debug, PartialEq)]
pub struct SharedState { pub struct SharedState {
pub board: BoardType, pub board: BoardType,
@ -119,64 +180,7 @@ impl Default for SharedState {
fn default() -> Self { fn default() -> Self {
Self { Self {
// cannot use [<type>; 56] because Rc does not impl Copy // cannot use [<type>; 56] because Rc does not impl Copy
board: [ board: new_empty_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())),
],
game_state: Rc::new(Cell::new(GameState::default())), game_state: Rc::new(Cell::new(GameState::default())),
turn: Rc::new(Cell::new(Turn::CyanPlayer)), turn: Rc::new(Cell::new(Turn::CyanPlayer)),
} }

View file

@ -1,7 +1,11 @@
use crate::constants::{COLS, INFO_TEXT_MAX_ITEMS, ROWS}; 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 crate::state::{BoardState, GameState, SharedState, Turn};
use std::cell::Cell; use std::cell::Cell;
use std::rc::Rc; use std::rc::Rc;
use yew::prelude::*; use yew::prelude::*;
pub struct MainMenu {} pub struct MainMenu {}
@ -117,6 +121,13 @@ impl Component for Slot {
.context::<SharedState>(Callback::noop()) .context::<SharedState>(Callback::noop())
.expect("state to be set"); .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 { if shared.game_state.get() == GameState::MainMenu {
return false; return false;
} }
@ -228,8 +239,8 @@ impl Component for Wrapper {
.link() .link()
.context::<SharedState>(Callback::noop()) .context::<SharedState>(Callback::noop())
.expect("state to be set"); .expect("state to be set");
let window = web_sys::window().expect("no window exists"); let (window, document) =
let document = window.document().expect("window should have a document"); get_window_document().expect("Should be able to get Window and Document");
match msg { match msg {
WrapperMsg::Pressed(idx) => { WrapperMsg::Pressed(idx) => {
@ -268,99 +279,82 @@ impl Component for Wrapper {
placed = true; placed = true;
} }
// DEBUG // check for win
//log::info!("{} is {:?}", idx, shared.board[idx as usize].get()); 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 let text_append_result =
//log::info!("{}", &output_str); 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);
}
shared
.game_state
.replace(GameState::PostGameResults(endgame_state));
} else {
// game is still ongoing
// info text below the grid // 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 { let output_str = match placed {
true => format!("{} placed into slot {}", current_player, bottom_idx), true => format!("{} placed into slot {}", current_player, bottom_idx),
false => "Invalid place to insert".into(), false => "Invalid place to insert".into(),
}; };
p.set_text_content(Some(&output_str));
// DEBUG let text_append_result = append_to_info_text(
//log::info!( &document,
// "pre: scroll top is {}, scroll height is {}", "info_text0",
// info_text.scroll_top(), &output_str,
// info_text.scroll_height() INFO_TEXT_MAX_ITEMS,
//); );
if let Err(e) = text_append_result {
// check if scrolled to top log::warn!("ERROR: text append to info_text0 failed: {}", e);
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");
} }
// 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());
}
// DEBUG
//log::info!(
// "post: scroll top is {}, scroll height is {}",
// info_text.scroll_top(),
// info_text.scroll_height()
//);
} else {
log::warn!("Failed to get bottom \"info_text\"");
} }
// info text right of the grid // 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(); let turn = shared.turn.get();
p.set_inner_html(&format!( let output_str = format!(
"<b class=\"{}\">It is {}'s turn</b>", "<b class=\"{}\">It is {}'s turn</b>",
turn.get_color(), turn.get_color(),
turn turn
)); );
// check if scrolled to top let text_append_result =
let at_top: bool = info_text.scroll_top() <= height - info_text.scroll_height(); append_to_info_text(&document, "info_text1", &output_str, 1);
if let Err(e) = text_append_result {
// append text to output log::warn!("ERROR: text append to info_text1 failed: {}", e);
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");
} }
// scroll to top only if at top
if at_top {
info_text.set_scroll_top(height - info_text.scroll_height());
}
} else {
log::warn!("Failed to get side \"info_text\"");
} }
} // else: game is still ongoing after logic check
} // WrapperMsg::Pressed(idx) => } // WrapperMsg::Pressed(idx) =>
} // match (msg) } // match (msg)