extern crate pancurses; pub use pancurses::Input::*; pub use pancurses::*; use std::fs::File; use std::io::Write; use crate::consulter::*; use crate::game::*; use crate::parser::*; #[derive(PartialEq)] pub enum GUIResult { Undo, Quit, Save, SwitchElement, Raw(String), } pub struct GameLog { action_window: Window, pieces_window: Window, has_colours: bool, white_colour: u8, black_colour: u8, num_lines: usize, offset: usize, action_lines: Vec, } impl GameLog { pub fn new( ypos: i32, xpos: i32, height: i32, width: i32, has_colours: bool, white_colour: u8, black_colour: u8, ) -> GameLog { let gl = GameLog { pieces_window: newwin(4, width, ypos, xpos), action_window: newwin(height - 5, width, ypos + 5, xpos), has_colours, white_colour, black_colour, num_lines: (height - 7) as usize, offset: 0, action_lines: Vec::new(), }; gl.action_window.keypad(true); gl.action_window.nodelay(false); gl } pub fn update(&mut self, game: &Game) { self.pieces_window.clear(); if self.has_colours { self.pieces_window.attrset(ColorPair(self.white_colour)); } self.pieces_window.addstr(format!( "W: {:2}", game.get_pieces(Player::White, Stone::Flat) )); self.pieces_window.attrset(Attribute::Normal); self.pieces_window.addstr(" "); if self.has_colours { self.pieces_window.attrset(ColorPair(self.black_colour)); } self.pieces_window.addstr(format!( "B: {:2}", game.get_pieces(Player::Black, Stone::Flat), )); self.pieces_window.attrset(Attribute::Normal); self.pieces_window .addstr(format!(" Variant: {}", game.get_start_type().to_string())); self.pieces_window.mv(1, 0); if self.has_colours { self.pieces_window.attrset(ColorPair(self.white_colour)); } self.pieces_window.addstr(format!( "WC: {}", game.get_pieces(Player::White, Stone::Capstone) )); self.pieces_window.attrset(Attribute::Normal); self.pieces_window.addstr(" "); if self.has_colours { self.pieces_window.attrset(ColorPair(self.black_colour)); } self.pieces_window.addstr(format!( "BC: {}", game.get_pieces(Player::Black, Stone::Capstone), )); self.pieces_window.attrset(Attribute::Normal); self.pieces_window .addstr(format!(" Ply: {}", game.get_ply() + 1)); self.pieces_window.mv(3, 0); self.pieces_window.addstr("Active player: "); match game.get_current_player() { Player::Black => { if self.has_colours { self.pieces_window.attrset(ColorPair(self.black_colour)); } } Player::White => { if self.has_colours { self.pieces_window.attrset(ColorPair(self.white_colour)); } } } self.pieces_window.addstr(game.get_current_player_name()); let new_lines = game.get_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(); } fn list_actions(&self) { self.action_window.clear(); 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, Character('S') => return GUIResult::Save, _ => (), } } } } } pub struct BoardGUI { board_window: Window, cell_windows: Vec, size: u8, cell_height: u8, cell_width: u8, hoff: i32, cur_cell: Position, has_colours: bool, white_colour: u8, black_colour: u8, } impl BoardGUI { fn draw_grid(&self) { let s: i32 = self.size as i32; let h: i32 = self.cell_height as i32; let w: i32 = self.cell_width as i32; for y in 0..h * s + 1 { for x in 0..w * s + 1 { if y % h == 0 { if x % w == 0 { self.board_window.mvaddch(y, x + self.hoff, '+'); } else { self.board_window.mvaddch(y, x + self.hoff, '-'); } } else if x % w == 0 { self.board_window.mvaddch(y, x + self.hoff, '|'); } } } for y in 0..s { self.board_window .mvaddstr(y * h + h / 2, 0, format!("{}.", s - y)); } for x in 0..s { self.board_window.mvaddstr( s * h + 1, w * x + w / 2 + self.hoff, format!("{:x}.", x + 10), ); } self.board_window.refresh(); } fn pos_to_idx(&self, pos: &Position) -> usize { let y = pos.y as usize; let x = pos.x as usize; x + y * (self.size as usize) } fn mv_to_cell(&self, pos: &Position, line: i32) { if let Some(win) = self.cell_windows.get(self.pos_to_idx(pos)) { win.mv(line, 0); win.refresh(); } } fn draw_stone(&self, win: &Window, top: bool, player: &Player, stone: &Stone) { if self.has_colours { match player { Player::Black => win.attrset(ColorPair(self.black_colour)), Player::White => win.attrset(ColorPair(self.white_colour)), }; if top { win.addch(match stone { Stone::Flat => '#', Stone::Standing => '/', Stone::Capstone => '*', }); } else { win.addstr(format!("{}", player)); } } else { win.addstr(format!("{}", player)); if top { win.addstr(format!("{}", stone)); } } } fn draw_stack(&self, pos: &Position, stack: &Stack) { if let Some(win) = self.cell_windows.get(self.pos_to_idx(pos)) { win.clear(); let height = stack.len() as usize; if height > 0 { let carry_capacity = self.size as usize; let tall = height > carry_capacity; if tall { win.attrset(Attribute::Normal); win.addch('('); } for i in 0..height { let piece = &stack[i]; self.draw_stone(win, i + 1 == height, &piece.player, &piece.stone); if tall && i + 1 == height - carry_capacity { win.attrset(Attribute::Normal); win.addch(')'); } } } win.refresh(); } } pub fn update(&self, game: &Game, squares: Vec) { // Is this really the `best' way? assert!(game.get_size() == self.size); for p in squares { if let Some(stack) = game.query_square(&p) { self.draw_stack(&p, stack); } } // Prevent redraw of the grid itself? self.board_window.untouch(); } /* pub fn read_action(&mut self) -> GUIResult { loop { if let Some(key) = self.board_window.getch() { match key { KeyDown => { if 0 < self.cur_cell.y { self.cur_cell.y -= 1; self.mv_to_cell(&self.cur_cell, 0); } } KeyUp => { if self.cur_cell.y + 1 < self.size { self.cur_cell.y += 1; self.mv_to_cell(&self.cur_cell, 0); } } KeyLeft => { if 0 < self.cur_cell.x { self.cur_cell.x -= 1; self.mv_to_cell(&self.cur_cell, 0); } } KeyRight => { if self.cur_cell.x + 1 < self.size { self.cur_cell.x += 1; self.mv_to_cell(&self.cur_cell, 0); } } Character('q') => (), x => { self.board_window.addstr(format!("{:?}", x)); self.board_window.refresh(); } } } } }*/ pub fn new( ypos: i32, xpos: i32, size: u8, cell_height: u8, cell_width: u8, hoff: u8, has_colours: bool, white_colour: u8, black_colour: u8, ) -> BoardGUI { let hoff = hoff as i32; let s: i32 = size as i32; let h: i32 = cell_height as i32; let w: i32 = cell_width as i32; let board_window = newwin(h * s + 3, w * s + 3 + hoff, ypos, xpos); let mut cell_windows: Vec = Vec::new(); for i in 0..s * s { let y: i32 = i / s; let x: i32 = i % s; cell_windows.push(newwin( h - 1, w - 1, (s - y - 1) * h + 1 + ypos, hoff + x * w + 1 + xpos, )); } let bg = BoardGUI { board_window: board_window, cell_windows: cell_windows, size: size, cell_height: cell_height, cell_width: cell_width, hoff: hoff, cur_cell: Position { x: 0, y: 0 }, has_colours: has_colours, black_colour: black_colour, white_colour: white_colour, }; bg.board_window.keypad(true); bg.board_window.nodelay(false); bg.draw_grid(); bg.mv_to_cell(&bg.cur_cell, 0); bg.board_window.refresh(); bg } } pub struct ActionEntry { window: Window, max_len: usize, xstart: i32, } impl ActionEntry { pub fn clear_entry_area(&self) { self.window.mv(0, self.xstart); for _i in 0..self.max_len { self.window.addch('_'); } self.window.mv(0, self.xstart); self.window.refresh(); } pub fn new(ypos: i32, xpos: i32, max_len: usize) -> ActionEntry { let txt = "Enter action: "; let xstart = txt.len() as i32; let width = max_len as i32 + xstart + 1; let ae = ActionEntry { window: newwin(1, width, ypos, xpos), max_len: max_len, xstart: xstart, }; ae.window.nodelay(false); ae.window.keypad(true); ae.window.addstr(txt); ae.clear_entry_area(); ae } pub fn read_action(&self) -> GUIResult { self.window.mv(0, self.xstart); self.window.refresh(); let mut result = String::new(); loop { if let Some(inp) = self.window.getch() { match inp { Character('\t') => return GUIResult::SwitchElement, Character('\n') => break, Character(c) => { // Annoyingly there are two backspace possibilities let ascii = c as usize; if !result.is_empty() && (ascii == 127) || (ascii == 8) { result.pop(); let (y, x) = (self.window.get_cur_y(), self.window.get_cur_x()); self.window.mv(y, x - 1); self.window.addch('_'); self.window.mv(y, x - 1); } else { if (result.len() < self.max_len) && (ascii > 32) && (ascii < 127) { result.push(c); self.window.addch(c); } } } KeyBackspace => { if !result.is_empty() { result.pop(); let (y, x) = (self.window.get_cur_y(), self.window.get_cur_x()); self.window.mv(y, x - 1); self.window.addch('_'); self.window.mv(y, x - 1); } } KeyEnter => break, _ => (), }; } self.window.refresh(); } self.clear_entry_area(); return GUIResult::Raw(result); } } pub struct MessageLog { window: Window, has_colours: bool, red_colour: u8, } impl MessageLog { pub fn new( ypos: i32, xpos: i32, height: i32, width: i32, has_colours: bool, red_colour: u8, ) -> MessageLog { MessageLog { window: newwin(height, width, ypos, xpos), has_colours: has_colours, red_colour: red_colour, } } pub fn log_message(&self, str: String) { self.window.clear(); if self.has_colours { self.window.attrset(Attribute::Normal); } self.window.addstr(str); self.window.refresh(); } pub fn log_error(&self, str: String) { self.window.clear(); if self.has_colours { self.window.attrset(ColorPair(self.red_colour)); } self.window.addstr(str); self.window.refresh(); } } enum InputSource { Player, Log, } const ACTION_HEADER: &str = "Enter action in PTN"; const LOG_HEADER: &str = "U to undo a turn | / to scroll | S to save PTN | Q to quit"; pub struct GUI { screen: Window, game_log: GameLog, pub message_log: MessageLog, action_entry: ActionEntry, board_gui: BoardGUI, input_source: InputSource, } impl GUI { fn write_header(&self, header: &str) { self.screen.mv(0, 0); self.screen.clrtoeol(); self.screen.addstr(format!( "takwrap {} | to switch GUI element | {}", env!("CARGO_PKG_VERSION").to_string(), header )); self.screen.mvchgat(0, 0, -1, A_REVERSE, -1); self.screen.refresh(); } fn save(&self, game: &Game) { let file = File::create(format!("Game_{}.ptn", game.get_date_string())); match file { Err(e) => { self.message_log .log_error(format!("Unable to open PTN file for writing: {}", e)); } Ok(file) => { if let Err(e) = write!(&file, "{}", game.to_string()) { self.message_log .log_error(format!("Unable to write PTN to file: {}", e)); } else { self.message_log.log_message(String::from("PTN saved.")); } } } } pub fn new(game: &Game, warning: String) -> GUI { let screen = initscr(); cbreak(); noecho(); curs_set(2); let has_colours = has_colors(); let (black_colour, white_colour, red_colour): (u8, u8, u8) = (1, 2, 3); 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 voff = 2; let height = screen.get_max_y() - voff; screen.refresh(); let cell_height = 4; let cell_width = 8; let hoff = 3; let size = game.get_size(); 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 game_log = GameLog::new( voff, board_right, height - 2, 30, has_colours, white_colour, black_colour, ); // Need room for '8___011111111', for example let action_entry = ActionEntry::new(board_bottom, 0, 13); let message_log = MessageLog::new( board_bottom + 2, 0, height - board_bottom - 1, board_right, has_colours, red_colour, ); let mut gui = GUI { screen, game_log, message_log, action_entry, board_gui, input_source: InputSource::Player, }; gui.write_header(ACTION_HEADER); gui.message_log.log_error(warning); gui.game_log.update(game); gui } pub fn query_input( &mut self, game: &Game, call_proc: bool, p1_white: bool, engine: &str, ) -> Result { match self.input_source { InputSource::Player => { let player = game.get_current_player(); 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()) } } }; match inp { Err(e) => Err(format!("Error: {}", e)), Ok(ok) => Ok(ok), } } InputSource::Log => Ok(self.game_log.read_action()), } } pub fn handle_input( &mut self, game: &mut Game, call_proc: bool, p1_white: bool, input: Result, ) -> bool { let player = game.get_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 => { self.message_log .log_message(String::from("Press any key to quit.")); self.screen.getch(); return false; } GUIResult::Save => { self.save(game); self.screen.getch(); return false; } GUIResult::SwitchElement => 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 => 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.get_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.get_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 \"{}\": p{}", 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!( "Game over: {}\nPress S to save PTN and quit, or Q to quit.", w )); loop { if let Some(inp) = self.screen.getch() { match inp { Character('S') => { self.save(game); return false; } Character('Q') => { return false; } _ => (), } } } } } } } } } }, } return true; } }