/*
This file is part of Takwrap.
Takwrap is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Foobar is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Foobar. If not, see .
*/
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 TUIResult {
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) -> TUIResult {
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 TUIResult::SwitchElement,
Character('U') => return TUIResult::Undo,
Character('Q') => return TUIResult::Quit,
Character('S') => return TUIResult::Save,
_ => (),
}
}
}
}
}
pub struct BoardTUI {
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 BoardTUI {
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, buried: bool, player: &Player, stone: &Stone) {
if self.has_colours && (!buried) {
match player {
Player::Black => win.attron(ColorPair(self.black_colour)),
Player::White => win.attron(ColorPair(self.white_colour)),
};
if top {
win.addch(match stone {
Stone::Flat => '#',
Stone::Standing => '/',
Stone::Capstone => '*',
});
} else {
win.addstr(format!("{}", player));
}
win.attrset(Attribute::Normal);
} else {
if buried {
win.attron(Attribute::Underline);
}
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 i32;
if height > 0 {
let carry_capacity = self.size;
let my = win.get_max_y();
let mut cy = 0;
let mut cx = 0;
for i in 0..height {
let piece = &stack[(height - i - 1) as usize];
self.draw_stone(
win,
i == 0,
i >= carry_capacity as i32,
&piece.player,
&piece.stone,
);
cy += 1;
if (cy >= my) && (i + 1 < height) {
cx += 1;
if i + my - 1 >= height {
cy = i + my - height + 1;
} else {
cy = 1;
}
win.mv(0, cx);
win.attrset(Attribute::Normal);
win.addch('v');
}
win.mv(cy, cx);
}
}
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 new(
ypos: i32,
xpos: i32,
size: u8,
cell_height: u8,
cell_width: u8,
hoff: u8,
has_colours: bool,
white_colour: u8,
black_colour: u8,
) -> BoardTUI {
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 = BoardTUI {
board_window,
cell_windows,
size,
cell_height,
cell_width,
hoff,
cur_cell: Position { x: 0, y: 0 },
has_colours,
black_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,
xstart,
};
ae.window.nodelay(false);
ae.window.keypad(true);
ae.window.addstr(txt);
ae.clear_entry_area();
ae
}
pub fn read_action(&self) -> TUIResult {
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 TUIResult::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 TUIResult::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 TUI {
screen: Window,
game_log: GameLog,
pub message_log: MessageLog,
action_entry: ActionEntry,
board_tui: BoardTUI,
input_source: InputSource,
}
impl TUI {
fn write_header(&self, header: &str) {
self.screen.mv(0, 0);
self.screen.clrtoeol();
self.screen.addstr(format!(
"takwrap {} | to switch TUI 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) -> TUI {
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_tui = BoardTUI::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 tui = TUI {
screen,
game_log,
message_log,
action_entry,
board_tui,
input_source: InputSource::Player,
};
tui.write_header(ACTION_HEADER);
tui.message_log.log_error(warning);
tui.game_log.update(game);
tui
}
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(tui_result) => match tui_result {
TUIResult::Quit => {
self.message_log
.log_message(String::from("Press any key to quit."));
self.screen.getch();
return false;
}
TUIResult::Save => {
self.save(game);
self.screen.getch();
return false;
}
TUIResult::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);
}
},
TUIResult::Undo => match game.undo() {
Ok(pos_vec) => {
self.message_log
.log_message(String::from("Game undone by one turn."));
self.board_tui.update(&game, pos_vec);
self.game_log.update(&game);
}
Err(err) => {
self.message_log.log_error(err);
}
},
TUIResult::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 \"{}\": {}",
act_string, e
));
}
}
Ok((pos_vec, win)) => {
self.board_tui.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;
}
}