use crate::ai::{get_ai_choice, AIDifficulty}; use crate::constants::{COLS, INFO_TEXT_MAX_ITEMS, ROWS}; use crate::game_logic::{check_win_draw, WinType}; use crate::html_helper::{ append_to_info_text, element_append_class, element_remove_class, get_window_document, }; use crate::random_helper::get_seeded_random; use crate::state::{BoardState, GameState, SharedState, Turn}; use std::cell::Cell; use std::rc::Rc; use yew::prelude::*; pub struct MainMenu {} pub enum MainMenuMessage { SinglePlayer(Turn, AIDifficulty), LocalMultiplayer, NetworkedMultiplayer, } impl Component for MainMenu { type Message = MainMenuMessage; type Properties = (); fn create(_ctx: &Context) -> Self { Self {} } fn view(&self, ctx: &Context) -> Html { let (shared, _) = ctx .link() .context::(Callback::noop()) .expect("state to be set"); match shared.game_state.get() { GameState::MainMenu => { let player_type: Turn; { let mut rng = get_seeded_random().expect("Random should be available"); player_type = if rng.rand_range(0..2) == 0 { Turn::CyanPlayer } else { Turn::MagentaPlayer }; } let easy_player_type = player_type; let normal_player_type = player_type; let hard_player_type = player_type; let onclick_singleplayer_easy = ctx.link().callback(move |_| { MainMenuMessage::SinglePlayer(easy_player_type, AIDifficulty::Easy) }); let onclick_singleplayer_normal = ctx.link().callback(move |_| { MainMenuMessage::SinglePlayer(normal_player_type, AIDifficulty::Normal) }); let onclick_singleplayer_hard = ctx.link().callback(move |_| { MainMenuMessage::SinglePlayer(hard_player_type, AIDifficulty::Hard) }); let onclick_local_multiplayer = ctx.link().callback(|_| MainMenuMessage::LocalMultiplayer); html! {
{"Please pick a game mode."}
} } _ => html! {
}, } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { let (shared, _) = ctx .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"); shared.game_state.replace(msg.into()); if shared.game_state.get() != GameState::MainMenu { let mainmenu = document .get_element_by_id("mainmenu") .expect("mainmenu should exist"); mainmenu.set_class_name("hidden_menu"); mainmenu.set_inner_html(""); let info_text_turn = document .get_element_by_id("info_text1") .expect("info_text1 should exist"); info_text_turn.set_inner_html("

It is CyanPlayer's Turn

"); if let GameState::SinglePlayer(Turn::MagentaPlayer, ai_difficulty) = shared.game_state.get() { // AI player starts first let choice = get_ai_choice(ai_difficulty, Turn::CyanPlayer, &shared.board) .expect("AI should have an available choice"); ctx.link() .get_parent() .expect("Wrapper should be parent of MainMenu") .clone() .downcast::() .send_message(WrapperMsg::Pressed(usize::from(choice) as u8)); } } true } } pub struct Slot {} pub enum SlotMessage { Press, } #[derive(Clone, PartialEq, Properties)] pub struct SlotProperties { idx: u8, state: Rc>, placed: Rc>, } impl Component for Slot { type Message = SlotMessage; type Properties = SlotProperties; fn create(_ctx: &Context) -> Self { Self {} } fn view(&self, ctx: &Context) -> Html { let idx = ctx.props().idx; let state = ctx.props().state.as_ref().get(); let onclick = ctx.link().callback(move |_| SlotMessage::Press); let col = idx % COLS; let row = idx / COLS; let place = if ctx.props().placed.get() && !state.is_win() { "placed" } else { "" }; ctx.props().placed.replace(false); html! { } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { let (shared, _) = ctx .link() .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; } match msg { SlotMessage::Press => { // notify Wrapper with message let msg = WrapperMsg::Pressed(ctx.props().idx); if let Some(p) = ctx.link().get_parent() { p.clone().downcast::().send_message(msg); } } } true } } pub struct Wrapper {} pub enum WrapperMsg { Pressed(u8), } impl Component for Wrapper { type Message = WrapperMsg; type Properties = (); fn create(_ctx: &Context) -> Self { Self {} } fn view(&self, ctx: &Context) -> Html { let (shared, _) = ctx .link() .context::(Callback::noop()) .expect("state to be set"); html! {
// wrapper } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { let (shared, _) = ctx .link() .context::(Callback::noop()) .expect("state to be set"); let (window, document) = get_window_document().expect("Should be able to get Window and Document"); match msg { WrapperMsg::Pressed(idx) => { let mut bottom_idx = idx; let mut placed = false; let current_player = shared.turn.get(); // check if clicked on empty slot if shared.board[idx as usize].get().is_empty() { // get bottom-most empty slot while bottom_idx + COLS < ROWS * COLS && shared.board[(bottom_idx + COLS) as usize].get().is_empty() { bottom_idx += COLS; } // apply current player's color to bottom-most empty slot shared.board[bottom_idx as usize].replace(shared.turn.get().into()); let current_board_state = shared.board[bottom_idx as usize].get(); // swap turn shared.turn.replace(shared.turn.get().get_opposite()); // get handle to slot if let Some(slot) = document.get_element_by_id(&format!("slot{bottom_idx}")) { // set slot info slot.set_class_name(&format!( "slot {} r{} c{} placed", current_board_state, bottom_idx / COLS, bottom_idx % COLS )); shared.placed[bottom_idx as usize].replace(true); } placed = true; } // 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); } } // 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); } } // check for win let check_win_draw_opt = check_win_draw(&shared.board); if let Some((endgame_state, win_type)) = 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); } shared .game_state .replace(GameState::PostGameResults(BoardState::Empty)); } 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); } shared .game_state .replace(GameState::PostGameResults(turn.into())); match win_type { WinType::Horizontal(idx) => { let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 1), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 2), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 3), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let append_result = element_append_class(&document, &format!("slot{}", idx), "win"); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 1), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 2), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 3), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } shared.board[idx].replace(shared.board[idx].get().into_win()); shared.board[idx + 1] .replace(shared.board[idx + 1].get().into_win()); shared.board[idx + 2] .replace(shared.board[idx + 2].get().into_win()); shared.board[idx + 3] .replace(shared.board[idx + 3].get().into_win()); } WinType::Vertical(idx) => { let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + (COLS as usize)), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 2 * (COLS as usize)), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 3 * (COLS as usize)), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let append_result = element_append_class(&document, &format!("slot{}", idx), "win"); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + (COLS as usize)), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 2 * (COLS as usize)), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 3 * (COLS as usize)), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } shared.board[idx].replace(shared.board[idx].get().into_win()); shared.board[idx + (COLS as usize)] .replace(shared.board[idx + (COLS as usize)].get().into_win()); shared.board[idx + 2 * (COLS as usize)].replace( shared.board[idx + 2 * (COLS as usize)].get().into_win(), ); shared.board[idx + 3 * (COLS as usize)].replace( shared.board[idx + 3 * (COLS as usize)].get().into_win(), ); } WinType::DiagonalUp(idx) => { let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 1 - (COLS as usize)), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 2 - 2 * (COLS as usize)), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 3 - 3 * (COLS as usize)), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let append_result = element_append_class(&document, &format!("slot{}", idx), "win"); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 1 - (COLS as usize)), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 2 - 2 * (COLS as usize)), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 3 - 3 * (COLS as usize)), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } shared.board[idx].replace(shared.board[idx].get().into_win()); shared.board[idx + 1 - (COLS as usize)].replace( shared.board[idx + 1 - (COLS as usize)].get().into_win(), ); shared.board[idx + 2 - 2 * (COLS as usize)].replace( shared.board[idx + 2 - 2 * (COLS as usize)].get().into_win(), ); shared.board[idx + 3 - 3 * (COLS as usize)].replace( shared.board[idx + 3 - 3 * (COLS as usize)].get().into_win(), ); } WinType::DiagonalDown(idx) => { let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 1 + (COLS as usize)), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 2 + 2 * (COLS as usize)), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let placed_class_erase_result = element_remove_class( &document, &format!("slot{}", idx + 3 + 3 * (COLS as usize)), "placed", ); if let Err(e) = placed_class_erase_result { log::warn!("ERROR: element_remove_class failed: {}", e); } let append_result = element_append_class(&document, &format!("slot{}", idx), "win"); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 1 + (COLS as usize)), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 2 + 2 * (COLS as usize)), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } let append_result = element_append_class( &document, &format!("slot{}", idx + 3 + 3 * (COLS as usize)), "win", ); if let Err(e) = append_result { log::warn!("ERROR: element_append_class failed: {}", e); } shared.board[idx].replace(shared.board[idx].get().into_win()); shared.board[idx + 1 + (COLS as usize)].replace( shared.board[idx + 1 + (COLS as usize)].get().into_win(), ); shared.board[idx + 2 + 2 * (COLS as usize)].replace( shared.board[idx + 2 + 2 * (COLS as usize)].get().into_win(), ); shared.board[idx + 3 + 3 * (COLS as usize)].replace( shared.board[idx + 3 + 3 * (COLS as usize)].get().into_win(), ); } WinType::None => unreachable!("WinType should never be None on win"), } } 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); } shared .game_state .replace(GameState::PostGameResults(endgame_state)); } // if: check for win or draw // check if it is AI's turn if let GameState::SinglePlayer(player_type, ai_difficulty) = shared.game_state.get() { if shared.turn.get() != player_type { // get AI's choice let choice = get_ai_choice(ai_difficulty, player_type.get_opposite(), &shared.board) .expect("AI should have an available choice"); ctx.link() .send_message(WrapperMsg::Pressed(usize::from(choice) as u8)); } } } // WrapperMsg::Pressed(idx) => } // match (msg) true } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct InfoText {} #[derive(Copy, Clone, Debug, PartialEq, Eq, Properties)] pub struct InfoTextProperties { id: usize, } impl Component for InfoText { type Message = (); type Properties = InfoTextProperties; fn create(_ctx: &Context) -> Self { Self {} } fn view(&self, ctx: &Context) -> Html { let (shared, _) = ctx .link() .context::(Callback::noop()) .expect("state to be set"); match ctx.props().id { 0 => { html! {
{"Hello"}
} } 1 => { if shared.game_state.get() == GameState::MainMenu { html! {

{"Waiting to choose game-mode..."}

} } else if shared.turn.get() == Turn::CyanPlayer { html! {

{"It is CyanPlayer's turn"}

} } else { html! {

{"It is MagentaPlayer's turn"}

} } } _ => { unreachable!(); } } } }