From e2849f91800479b2734696e0501b54bbcd6945d2 Mon Sep 17 00:00:00 2001 From: Vegard Matthey Date: Fri, 15 Aug 2025 14:12:09 +0200 Subject: [PATCH] update project name --- Cargo.toml | 3 +- src/main.rs | 257 +++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 197 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f24512e..8078523 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [package] -name = "term_chess" +name = "term-chess" version = "0.1.0" edition = "2021" [dependencies] +crossterm = "0.27.0" diff --git a/src/main.rs b/src/main.rs index 37e9d5d..3c77f98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,10 @@ +use crossterm::{ + event, + event::{EnableMouseCapture, Event, KeyCode, MouseButton, MouseEventKind}, + execute, terminal, +}; +use std::io; + const SIZE: usize = 8; const WHITE: Color = Color { r: 255, @@ -22,16 +29,24 @@ enum SpecialMoveError { SquareAttacked, } -#[derive(Debug)] -enum InputParseError { - MissingCoordinate, - NumberParseError, -} - struct Game { board: Board, turn: Side, - moves: Vec<(Position, Position)>, + moves: Vec, +} + +struct FinishedGame { + board: Board, + turn: Side, + moves: Vec, + move_point: usize, +} + +struct Move { + from: Position, + to: Position, + piece: Option, + special_move: Option, } #[derive(Debug, Clone)] @@ -42,7 +57,7 @@ struct Piece { move_count: u32, } -#[derive(PartialEq)] +#[derive(PartialEq, Clone)] enum SpecialMove { Castle((bool, bool)), EnPassant(Position), @@ -74,27 +89,41 @@ struct Position { struct Board([[Option; SIZE]; SIZE]); fn main() { + let mut stdout = io::stdout(); let mut game = Game::new(); let mut msg = String::new(); - game.board.render("White to play."); + let mut s = String::with_capacity(2500); + game.board.render(&mut s, "White to play."); loop { - let start_pos = get_input("Select piece: "); + let start_pos = input(&mut stdout); let res_pos = Position::new(start_pos.x, start_pos.y); if let Some(piece) = &game.board.0[start_pos.y][start_pos.x] { let mut piece = piece.clone(); if piece.side == game.turn { - let pos = get_input("Select square: "); - let move_result = piece.move_piece(&pos, &game.board, game.moves.last()); + let pos = input(&mut stdout); + + let move_pos = if let Some(m) = game.moves.last() { + Some((m.from.clone(), m.to.clone())) + } else { + None + }; + + let move_result = piece.move_piece(&pos, &game.board, move_pos.as_ref()); if let Ok(special_move) = move_result { - if handle_special_moves(special_move, &mut game, &mut piece, &start_pos, &pos) + if handle_special_moves(&special_move, &mut game, &mut piece, &start_pos, &pos) .is_ok() { game.turn = match game.turn { Side::White => Side::Black, Side::Black => Side::White, }; - game.moves.push((start_pos, pos)); + game.moves.push(Move::new( + start_pos, + pos.clone(), + game.board.0[pos.y][pos.x].clone(), + special_move, + )); piece.move_count += 1; } else { piece.pos = start_pos; @@ -107,8 +136,15 @@ fn main() { let (x, y) = (piece.pos.x, piece.pos.y); game.board.0[res_pos.y][res_pos.x] = None; game.board.0[y][x] = Some(piece); - if game.board.count_moves(&game.turn, game.moves.last()) == 0 { - if let Some(loser) = game.board.check_for_check(game.moves.last()) { + + let move_pos = if let Some(m) = game.moves.last() { + Some((m.from.clone(), m.to.clone())) + } else { + None + }; + + if game.board.count_moves(&game.turn, move_pos.as_ref()) == 0 { + if let Some(loser) = game.board.check_for_check(move_pos.as_ref()) { msg = match loser { Side::White => "Black wins with checkmate!".to_string(), Side::Black => "White wins with checkmate!".to_string(), @@ -116,24 +152,41 @@ fn main() { } else { msg = "Stalemate!".to_string(); } - game.board.render(&msg); + game.board.render(&mut s, &msg); break; } } } else { msg.push_str("Coordinates do not correspond to piece. "); + msg.push_str("pos: "); + msg.push_str(&start_pos.x.to_string()); + msg.push_str(", "); + msg.push_str(&start_pos.y.to_string()); } match game.turn { Side::White => msg.push_str("White to play."), Side::Black => msg.push_str("Black to play."), } - game.board.render(&msg); + game.board.render(&mut s, &msg); msg.clear(); } + std::thread::sleep(std::time::Duration::from_millis(500)); + + s.clear(); + game.board = Board::new(); + game.turn = Side::White; + for i in 0..game.moves.len() { + game.board.render(&mut s, ""); + std::thread::sleep(std::time::Duration::from_millis(500)); + game.next_move(i); + } + game.board.render(&mut s, ""); + std::thread::sleep(std::time::Duration::from_millis(500)); + exit(); } fn handle_special_moves( - special_move: Option, + special_move: &Option, game: &mut Game, piece: &mut Piece, start_pos: &Position, @@ -148,15 +201,22 @@ fn handle_special_moves( SpecialMove::Castle((left, right)) => { assert!(piece.kind == PieceKind::King); assert!(start_pos.y == pos.y); + + let move_pos = if let Some(m) = game.moves.last() { + Some((m.from.clone(), m.to.clone())) + } else { + None + }; + match start_pos.x.abs_diff(pos.x) { 2 => { - if right { + if *right { assert_eq!(start_pos.x, 4); for x in start_pos.x..SIZE { if game.board.is_attacked( &Position::new(x, piece.pos.y), &piece.side, - game.moves.last(), + move_pos.as_ref(), ) { return Err(SpecialMoveError::SquareAttacked); } @@ -166,7 +226,7 @@ fn handle_special_moves( rook.move_piece( &Position::new(pos.x - 1, pos.y), &game.board, - game.moves.last(), + move_pos.as_ref(), ) .expect("f rook"); game.board.0[pos.y][pos.x - 1] = Some(rook); @@ -178,14 +238,14 @@ fn handle_special_moves( } } 3 => { - if left { + if *left { assert!(game.board.0[piece.pos.y][0].is_some()); assert_eq!(start_pos.x, 4); for x in 0..start_pos.x + 1 { if game.board.is_attacked( &Position::new(x, piece.pos.y), &piece.side, - game.moves.last(), + move_pos.as_ref(), ) { return Err(SpecialMoveError::SquareAttacked); } @@ -194,7 +254,7 @@ fn handle_special_moves( rook.move_piece( &Position::new(pos.x + 1, pos.y), &game.board, - game.moves.last(), + move_pos.as_ref(), ) .expect("f moving rook"); game.board.0[pos.y][pos.x + 1] = Some(rook); @@ -219,36 +279,101 @@ impl Game { moves: Vec::new(), } } + fn next_move(&mut self, move_point: usize) { + let piece_move = &self.moves[move_point]; + let (start, end) = ( + self.moves[move_point].from.clone(), + self.moves[move_point].to.clone(), + ); + let mut piece = self.board.0[piece_move.from.y][piece_move.from.x] + .clone() + .unwrap(); + self.board.0[piece_move.to.y][piece_move.to.x] = + self.board.0[piece_move.from.y][piece_move.from.x].take(); + handle_special_moves( + &self.moves[move_point].special_move.clone(), + self, + &mut piece, + &start, + &end, + ) + .unwrap(); + } } -fn get_input(query: &str) -> Position { - use std::{io, io::Write}; - let mut s = String::new(); - print!("{}", query); - io::stdout().flush().expect("failed to flush stdout"); - io::stdin().read_line(&mut s).expect("failed to read stdin"); - match parse_input(&s) { - Ok(v) => v, - Err(e) => { - eprintln!("string: {s} failed to parse: {:?}", e); - std::process::exit(1); +impl Move { + fn new( + from: Position, + to: Position, + piece: Option, + special_move: Option, + ) -> Self { + Self { + from, + to, + piece, + special_move, } } } -fn parse_input(input: &str) -> Result { - let mut args = input.trim().split(','); - let (x, y) = ( - args.next() - .ok_or(InputParseError::MissingCoordinate)? - .parse() - .map_err(|_| InputParseError::NumberParseError)?, - args.next() - .ok_or(InputParseError::MissingCoordinate)? - .parse() - .map_err(|_| InputParseError::NumberParseError)?, - ); - Ok(Position { x, y }) +impl FinishedGame { + fn new(moves: Vec) -> Self { + Self { + board: Board::new(), + turn: Side::White, + move_point: 0, + moves, + } + } + + fn next_move(&mut self) { + let piece_move = &self.moves[self.move_point]; + self.board.0[piece_move.to.y][piece_move.to.x] = + self.board.0[piece_move.from.y][piece_move.from.x].take(); + // handle_special_moves(&self.moves[self.move_point].special_move, &mut self.); + self.move_point += 1; + } + + fn prev_move(&mut self) { + let piece_move = &self.moves[self.move_point]; + self.board.0[piece_move.from.y][piece_move.from.x] = + self.board.0[piece_move.to.y][piece_move.to.x].take(); + self.board.0[piece_move.to.y][piece_move.to.x] = piece_move.piece.clone(); + } +} + +fn input(stdout: &mut io::Stdout) -> Position { + execute!(stdout, EnableMouseCapture).unwrap(); + loop { + if event::poll(std::time::Duration::MAX).unwrap() { + while let Ok(r) = event::read() { + if let Event::Mouse(event) = r { + if let MouseEventKind::Down(MouseButton::Left) = event.kind { + let (x, y) = ( + ((event.column as i32 - 1) / 2) as usize, + (9 - event.row as i32) as usize, + ); + if x > 7 || y > 7 { + continue; + } + let pos = Position::new(x, y); + return pos; + } + } else if let Event::Key(key) = r { + if key.code == KeyCode::Char('q') { + exit(); + } + } + } + } + } +} + +fn exit() -> ! { + print!("\x1b[?25h"); + terminal::disable_raw_mode().unwrap(); + std::process::exit(0); } impl Board { @@ -298,31 +423,32 @@ impl Board { false } - fn render(&self, msg: &str) { - // let mut s = String::with_capacity((SIZE + 2) * (SIZE * 2 + 3)); - let mut s = String::new(); - s.push('+'); - s.push_str(&"-".repeat(SIZE * 2)); - s.push('+'); + fn render(&self, s: &mut String, msg: &str) { + s.clear(); + s.push('┏'); + s.push_str(&"━".repeat(SIZE * 2)); + s.push('┓'); s.push('\n'); for (y, rank) in self.0.iter().rev().enumerate() { - s.push('|'); + s.push('┃'); for (x, square) in rank.iter().enumerate() { let bg_color = if (x + y) % 2 == 0 { WHITE } else { BLACK }; if let Some(piece) = square { let fg_color = if piece.side == Side::White { BLUE } else { RED }; - s.push_str(&piece.kind.to_string().fg(fg_color).bg(bg_color)); + s.push_str(&piece.kind.to_string().fg(fg_color).bg(bg_color).bold()); } else { s.push_str(&" ".bg(bg_color)); } } - s.push_str(&format!("|{}\n", (SIZE - y))); + s.push_str(&format!("┃{}\n", (SIZE - y))); } - s.push('+'); - s.push_str(&"-".repeat(SIZE * 2)); - s.push_str("+\n A B C D E F G H "); - print!("\u{001b}c"); + s.push('┗'); + s.push_str(&"━".repeat(SIZE * 2)); + s.push_str("┛\n A B C D E F G H "); + print!("\x1b[?25l\x1b[2J\x1b[H"); + terminal::disable_raw_mode().unwrap(); println!("{}\n{}", msg, s); + terminal::enable_raw_mode().unwrap(); } fn new() -> Self { Board([ @@ -565,6 +691,13 @@ impl Piece { return Err(MoveError::InCheck); } } + + match (self.side.clone(), self.pos.y) { + (Side::White, 7) => self.kind = PieceKind::Queen, + (Side::Black, 0) => self.kind = PieceKind::Queen, + _ => (), + } + Ok(special_move) }