diff options
| -rw-r--r-- | src/game.rs | 46 | ||||
| -rw-r--r-- | src/gui.rs | 68 | ||||
| -rw-r--r-- | src/main.rs | 423 |
3 files changed, 357 insertions, 180 deletions
diff --git a/src/game.rs b/src/game.rs index 5db2c48..4a80ce7 100644 --- a/src/game.rs +++ b/src/game.rs @@ -743,23 +743,28 @@ impl Game { self.size } - pub fn query_action_lines(&self) -> String { - let mut result = String::new(); + pub fn query_action_lines(&self) -> Vec<String> { + let mut result = Vec::new(); let mut newline = true; let num_actions = self.actions.len(); if num_actions > 0 { - result += "1. "; + let mut line = String::new(); + line += "1. "; for i in 0..num_actions { if i > 1 && newline { // TODO: Is placing the opponent's first stone the zeroeth action? - result += &format!("{}. ", i / 2 + 1); + line += &format!("{}. ", i / 2 + 1); } - result += &format!("{} ", self.actions[i]); + line += &format!("{} ", self.actions[i].to_string()); if i > 0 && !newline { - result.push('\n'); + result.push(line); + line = String::new(); } newline = !newline; } + if !line.is_empty() { + result.push(line); + } } result } @@ -834,6 +839,33 @@ impl Game { } } } + + pub fn undo(&mut self) -> Result<Vec<Position>, String> { + if self.actions.len() > 0 { + self.actions.pop(); + self.states.pop(); + self.turn_order = match self.actions.len() { + 0 => TurnOrder::WhitePlacesBlack, + 1 => TurnOrder::BlackPlacesWhite, + _ => TurnOrder::Normal, + }; + self.current_player = match self.current_player { + Player::Black => Player::White, + Player::White => Player::Black, + }; + // I'm too lazy to work out exactly which squares must be + // redrawn when an action is undone + let mut redraw_all: Vec<Position> = Vec::new(); + for x in 0..self.size { + for y in 0..self.size { + redraw_all.push(Position { x, y }); + } + } + Ok(redraw_all) + } else { + Err(String::from("Cannot undo in an empty game!")) + } + } } impl fmt::Display for Game { @@ -845,7 +877,7 @@ impl fmt::Display for Game { self.white_player_name, self.black_player_name, self.size, - self.query_action_lines() + self.query_action_lines().join("\n") ) } } @@ -8,6 +8,7 @@ use crate::game::*; #[derive(PartialEq)] pub enum GUIResult { Undo, + Quit, SwitchElement, Raw(String), } @@ -18,6 +19,9 @@ pub struct GameLog { has_colours: bool, white_colour: u8, black_colour: u8, + num_lines: usize, + offset: usize, + action_lines: Vec<String>, } impl GameLog { @@ -33,17 +37,20 @@ impl GameLog { let gl = GameLog { pieces_window: newwin(4, width, ypos, xpos), action_window: newwin(height - 5, width, ypos + 5, xpos), - has_colours: has_colours, - white_colour: white_colour, - black_colour: black_colour, + has_colours, + white_colour, + black_colour, + num_lines: (height - 7) as usize, + offset: 0, + action_lines: Vec::new(), }; - gl.action_window.setscrreg(0, height); - gl.action_window.scrollok(true); + gl.action_window.keypad(true); + gl.action_window.nodelay(false); gl } - pub fn update(&self, game: &Game) { + pub fn update(&mut self, game: &Game) { self.pieces_window.clear(); if self.has_colours { @@ -104,12 +111,55 @@ impl GameLog { } self.pieces_window.addstr(game.query_current_player_name()); + let new_lines = game.query_action_lines(); + if new_lines.len() > self.num_lines { + self.offset = new_lines.len() - self.num_lines; + } else { + self.offset = 0; + } + self.action_lines = new_lines.clone(); + self.list_actions(); + self.pieces_window.refresh(); + } - // Perhaps it's best to avoid clearing things? Overwrite the specific characters instead? + fn list_actions(&self) { self.action_window.clear(); - self.action_window.addstr(game.query_action_lines()); + self.action_window.draw_box(0, 0); + let mut y = 1; + for i in 0..self.num_lines { + if let Some(line) = self.action_lines.get(i + self.offset) { + self.action_window.mv(y, 2); + self.action_window.addstr(line); + y += 1; + } + } self.action_window.refresh(); + self.action_window.mv(1, 2); + } + + pub fn read_action(&mut self) -> GUIResult { + loop { + self.list_actions(); + if let Some(inp) = self.action_window.getch() { + match inp { + KeyUp => { + if self.offset > 0 { + self.offset -= 1; + } + } + KeyDown => { + if self.offset + self.num_lines < self.action_lines.len() { + self.offset += 1; + } + } + Character('\t') => return GUIResult::SwitchElement, + Character('U') => return GUIResult::Undo, + Character('Q') => return GUIResult::Quit, + _ => (), + } + } + } } } @@ -389,7 +439,7 @@ impl ActionEntry { self.window.addch('_'); self.window.mv(y, x - 1); } else { - if result.len() < self.max_len { + if (result.len() < self.max_len) && (ascii > 32) && (ascii < 127) { result.push(c); self.window.addch(c); } diff --git a/src/main.rs b/src/main.rs index 5fe8c18..e5ba074 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,98 +16,283 @@ use chrono::Local; use std::env; use std::time::SystemTime; +enum InputSource { + Player, + Log, +} + struct GUI { screen: Window, - // width: i32, - // height: i32, game_log: GameLog, message_log: MessageLog, action_entry: ActionEntry, board_gui: BoardGUI, + input_source: InputSource, } -fn init_gui(size: u8) -> GUI { - let screen = initscr(); - cbreak(); - noecho(); - curs_set(2); +impl GUI { + fn write_header(&self, header: &str) { + self.screen.mv(0, 0); + self.screen.clrtoeol(); + self.screen.addstr(format!( + "takwrap {} | <TAB> to switch GUI element | {}", + env!("CARGO_PKG_VERSION").to_string(), + header + )); + self.screen.mvchgat(0, 0, -1, A_REVERSE, -1); + self.screen.refresh(); + } - let has_colours = has_colors(); - let (black_colour, white_colour, red_colour): (u8, u8, u8) = (1, 2, 3); + fn new(size: u8) -> GUI { + let screen = initscr(); + cbreak(); + noecho(); + curs_set(2); - if has_colours { - start_color(); - use_default_colors(); - init_pair(black_colour as i16, -1, COLOR_RED); - init_pair(white_colour as i16, -1, COLOR_BLUE); - init_pair(red_colour as i16, COLOR_RED, -1); - } + let has_colours = has_colors(); + let (black_colour, white_colour, red_colour): (u8, u8, u8) = (1, 2, 3); - screen.addstr(format!( - "takwrap {} | <TAB> to switch GUI element", - env!("CARGO_PKG_VERSION").to_string() - )); + if has_colours { + start_color(); + use_default_colors(); + init_pair(black_colour as i16, -1, COLOR_RED); + init_pair(white_colour as i16, -1, COLOR_BLUE); + init_pair(red_colour as i16, COLOR_RED, -1); + } - screen.mvchgat(0, 0, -1, A_REVERSE, -1); - let voff = 2; + let voff = 2; - let width = screen.get_max_x(); - let height = screen.get_max_y() - voff; + // let width = screen.get_max_x(); + let height = screen.get_max_y() - voff; - screen.refresh(); + screen.refresh(); + + let cell_height = 4; + let cell_width = 8; + let hoff = 3; + + let board_bottom = voff + (size * cell_height + 3) as i32; + let board_right = (hoff + size * cell_width + 3) as i32; + + let board_gui = BoardGUI::new( + voff, + 0, + size, + cell_height, + cell_width, + hoff, + has_colours, + white_colour, + black_colour, + ); - let cell_height = 4; - let cell_width = 8; - let hoff = 3; + let game_log = GameLog::new( + voff, + board_right, + height - 2, + 30, + has_colours, + white_colour, + black_colour, + ); - let board_bottom = voff + (size * cell_height + 3) as i32; - let board_right = (hoff + size * cell_width + 3) as i32; + // Need room for '8___011111111', for example + let action_entry = ActionEntry::new(board_bottom, 0, 13); - let board_gui = BoardGUI::new( - voff, - 0, - size, - cell_height, - cell_width, - hoff, - has_colours, - white_colour, - black_colour, - ); + let message_log = MessageLog::new( + board_bottom + 2, + 0, + height - board_bottom - 1, + board_right, + has_colours, + red_colour, + ); - let game_log = GameLog::new( - voff, - board_right, - height, - width - board_right, - has_colours, - white_colour, - black_colour, - ); + GUI { + screen, + game_log, + message_log, + action_entry, + board_gui, + input_source: InputSource::Player, + } + } - // Need room for '8___011111111', for example - let action_entry = ActionEntry::new(board_bottom, 0, 13); + fn query_input( + &mut self, + game: &Game, + call_proc: bool, + p1_white: bool, + engine: &str, + ) -> Result<GUIResult, String> { + match self.input_source { + InputSource::Player => { + let player = game.query_current_player(); - let message_log = MessageLog::new( - board_bottom + 2, - 0, - height - board_bottom - 1, - board_right, - has_colours, - red_colour, - ); + let inp = match player { + Player::Black => { + if call_proc && p1_white { + self.message_log + .log_message(String::from("Opponent is thinking...")); + consult_next_action(&game, &engine) + } else { + Ok(self.action_entry.read_action()) + } + } + Player::White => { + if call_proc && (!p1_white) { + self.message_log + .log_message(String::from("Opponent is thinking...")); + consult_next_action(&game, &engine) + } else { + Ok(self.action_entry.read_action()) + } + } + }; - GUI { - screen, - // width, - // height, - game_log, - message_log, - action_entry, - board_gui, + match inp { + Err(e) => Err(format!("Error: {}", e)), + Ok(ok) => Ok(ok), + } + } + InputSource::Log => Ok(self.game_log.read_action()), + } + } + + fn handle_input( + &mut self, + game: &mut Game, + call_proc: bool, + p1_white: bool, + input: Result<GUIResult, String>, + ) -> bool { + let player = game.query_current_player(); + let engine_turn = call_proc + && ((p1_white && player == Player::Black) || ((!p1_white) && player == Player::White)); + + match input { + Err(e) => { + if engine_turn { + self.message_log + .log_error(format!("Unrecoverable error: {}", e)); + return false; + } + } + Ok(gui_result) => match gui_result { + GUIResult::Quit => { + if engine_turn { + self.message_log.log_error(String::from( + "Internal error: engine passed quit message somehow. Press any key to quit.", + )); + } else { + self.message_log + .log_message(String::from("Press any key to quit.")); + } + self.screen.getch(); + return false; + } + GUIResult::SwitchElement => { + if engine_turn { + self.message_log.log_error(String::from( + "Internal error: engine passed switch-element message somehow. Press any key to quit.", + )); + self.screen.getch(); + return false; + } else { + match self.input_source { + InputSource::Player => { + self.input_source = InputSource::Log; + self.write_header(LOG_HEADER); + } + InputSource::Log => { + self.input_source = InputSource::Player; + self.write_header(ACTION_HEADER); + } + } + } + } + GUIResult::Undo => { + if engine_turn { + self.message_log.log_error(String::from( + "Internal error: engine passed undo message somehow. Press any key to quit.", + )); + self.screen.getch(); + return false; + } else { + match game.undo() { + Ok(pos_vec) => { + self.message_log + .log_message(String::from("Game undone by one turn.")); + self.board_gui.update(&game, pos_vec); + self.game_log.update(&game); + } + Err(err) => { + self.message_log.log_error(err); + } + } + } + } + GUIResult::Raw(raw) => { + let stone_owner = game.query_stone_owner(); + let act = parse_action(stone_owner, &raw); + match act { + Err(err) => { + self.message_log + .log_error(format!("Input \"{}\": {}", raw, err)); + if engine_turn { + return false; + } + } + Ok(act) => { + let pn = game.query_current_player_name(); + self.message_log.log_message(format!( + "{} performs {}{}", + pn, + act, + if stone_owner != player { + format!(" with a {} stone.", stone_owner) + } else { + String::from(".") + } + )); + let act_string = act.to_string(); + match game.perform_action(act) { + Err(e) => { + if engine_turn { + self.message_log.log_error(String::from( + "Error: external and internal game states disagree. Press any key to quit.", + )); + self.screen.getch(); + return false; + } else { + self.message_log.log_error(format!( + "Cannot perform \"{}\": {}", + act_string, e + )); + } + } + + Ok((pos_vec, win)) => { + self.board_gui.update(&game, pos_vec); + self.game_log.update(&game); + if let Some(w) = win { + self.message_log.log_message(format!("{}", w)); + return false; + } + } + } + } + } + } + }, + } + return true; } } +const ACTION_HEADER: &str = "Enter action in PTN"; +const LOG_HEADER: &str = "U to undo a turn | <UP>/<DOWN> to scroll | Q to quit"; + fn main() { let mut p1_name = match env::var("USER") { Err(_) => String::from("Player1"), @@ -174,7 +359,7 @@ fn main() { call_proc = false; } - let gui = init_gui(size); + let mut gui = GUI::new(size); let p1_white; if !name.is_empty() { @@ -204,105 +389,15 @@ fn main() { gui.message_log.log_error(warning); gui.game_log.update(&game); - loop { - let player = game.query_current_player(); - let engine_turn = call_proc - && ((p1_white && player == Player::Black) || ((!p1_white) && player == Player::White)); - - let result = match player { - Player::Black => { - if call_proc && p1_white { - gui.message_log - .log_message(String::from("Opponent is thinking...")); - consult_next_action(&game, &engine) - } else { - Ok(gui.action_entry.read_action()) - } - } - Player::White => { - if call_proc && (!p1_white) { - gui.message_log - .log_message(String::from("Opponent is thinking...")); - consult_next_action(&game, &engine) - } else { - Ok(gui.action_entry.read_action()) - } - } - }; + gui.write_header(ACTION_HEADER); - match result { - Err(e) => { - gui.message_log.log_error(format!("Engine error: {}", e)); - break; - } - Ok(gui_result) => match gui_result { - GUIResult::SwitchElement => { - if engine_turn { - gui.message_log.log_error(String::from( - "Internal error: engine passed switch-element message somehow.", - )); - break; - } - } - GUIResult::Undo => { - if engine_turn { - gui.message_log.log_error(String::from( - "Internal error: engine passed undo message somehow.", - )); - break; - } - } - GUIResult::Raw(raw) => { - let stone_owner = game.query_stone_owner(); - let act = parse_action(stone_owner, &raw); - match act { - Err(err) => { - gui.message_log - .log_error(format!("Input \"{}\": {}", raw, err)); - if engine_turn { - break; - } - } - Ok(act) => { - let pn = game.query_current_player_name(); - gui.message_log.log_message(format!( - "{} performs {}{}", - pn, - act, - if stone_owner != player { - format!(" with a {} stone.", stone_owner) - } else { - String::from(".") - } - )); - match game.perform_action(act) { - Err(e) => { - gui.message_log.log_error(e); - if engine_turn { - gui.message_log.log_error(String::from( - "Error: external and internal game states disagree.", - )); - break; - } - } - - Ok((pos_vec, win)) => { - gui.board_gui.update(&game, pos_vec); - gui.game_log.update(&game); - if let Some(w) = win { - gui.message_log.log_message(format!("{}", w)); - break; - } - } - } - } - } - } - }, + loop { + let inp = gui.query_input(&game, call_proc, p1_white, &engine); + if !gui.handle_input(&mut game, call_proc, p1_white, inp) { + break; } } - gui.screen.getch(); // board_gui.read_action(); endwin(); } |
