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 {
return None;
if !has_empty_slot {
return Some(BoardState::Empty);
}
let check_result = |state| -> Option<BoardState> {
@ -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
);
}
}
}
}
}

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 constants;
mod game_logic;
mod html_helper;
mod random_helper;
mod state;
mod yew_components;

View File

@ -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<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,
@ -119,64 +180,7 @@ impl Default for SharedState {
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)),
}

View File

@ -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::<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;
}
@ -228,8 +239,8 @@ impl Component for Wrapper {
.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) => {
@ -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 <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();
// 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 <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");
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)