CHESS IN RUST
Mój ostatni projekt — pełnoprawny, konsolowy silnik szachowy napisany w języku Rust (edycja 2024) od zera.
A B C D E F G H
_______________________________
8 | ♜ | ♞ | ♝ | ♛ | ♚ | ♝ | ♞ | ♜ |
_______________________________
7 | ♟ | ♟ | ♟ | ♟ | ♟ | ♟ | ♟ | ♟ |
_______________________________
6 | | | | | | | | |
_______________________________
5 | | | | | | | | |
_______________________________
4 | | | | | | | | |
_______________________________
3 | | | | | | | | |
_______________________________
2 | ♙ | ♙ | ♙ | ♙ | ♙ | ♙ | ♙ | ♙ |
_______________________________
1 | ♖ | ♘ | ♗ | ♕ | ♔ | ♗ | ♘ | ♖ |
_______________________________
choose your move in this format: (for example e2 e4):
> e2 e4
White Pawn
"e2" moves to "e4"
choose your move in this format: (for example e7 e5):
> e7 e5
Black Pawn
"e7" moves to "e5"
// Sprawdzenie nielegalnego ruchu (odkrycie szacha):
> f2 f3
Illegal move
Architektura i Kluczowe Fragmenty Kodu
Zamiast używać niebezpiecznych wskaźników lub liczb całkowitych, każda bierka modelowana jest przez silnie typowane enumy Rusta: Color oraz Type.
Plansza szachowa to bezpieczna macierz 8x8 obiektów Option<Figure> — puste pole to po prostu None, a zajęte to Some(Figure).
Dzięki dyrektywom #[derive(Clone, Copy, Debug, PartialEq)] kopiowanie stanu planszy i porównywanie figur odbywa się z zerowym kosztem wydajnościowym.
// src/piece.rs
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Color {
White,
Black,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Type {
Pawn,
Rook,
Knight,
Bishop,
Queen,
King,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Figure {
pub color: Color,
pub ty: Type,
}
// src/board.rs
pub type Board = [[Option<Figure>; 8]; 8];
pub fn create_starting_board() -> Board {
let mut board = [[None; 8]; 8];
for i in 0..8 {
let figures: [Type; 8] = [
Type::Rook, Type::Knight, Type::Bishop, Type::Queen,
Type::King, Type::Bishop, Type::Knight, Type::Rook,
];
board[0][i] = Some(Figure { color: Color::White, ty: figures[i] });
board[1][i] = Some(Figure { color: Color::White, ty: Type::Pawn });
board[6][i] = Some(Figure { color: Color::Black, ty: Type::Pawn });
board[7][i] = Some(Figure { color: Color::Black, ty: figures[i] });
}
board
}
Ruchy bierek obliczane są w funkcji is_pseudo_legal_move. Skoczek (Knight) porusza się w kształcie litery „L” — zamiast zagnieżdżonych 8 if-ów, wykorzystana została zwarta tablica przesunięć 9-elementowa knight_offsets: [isize; 9] = [2, -1, -2, 1, -2, -1, 2, 1, 2], gdzie para współrzędnych to [i] oraz [i+1]. Dla gońca, wieży i hetmana zastosowano pętle promieniowe 1..8, które przerywają sprawdzanie (break) po napotkaniu przeszkody lub zbiciu wrogiej figury.
// Generowanie wszystkich ruchów dla skoczka z użyciem offsetów:
if figure.ty == Type::Knight {
let knight_offsets: [isize; 9] = [2, -1, -2, 1, -2, -1, 2, 1, 2];
for i in 0..8 {
let x = start[0] as isize + knight_offsets[i];
let y = start[1] as isize + knight_offsets[i + 1];
if (0..8).contains(&x) && (0..8).contains(&y) {
let target = [x as usize, y as usize];
if board[target[1]][target[0]].is_none() || is_enemy_piece_at(board, start, target) {
options.push([x as usize, y as usize]);
}
}
}
}
// Promienie gońca (Bishop) - raycasting po 4 przekątnych:
if figure.ty == Type::Bishop {
let bishop_offsets: [[isize; 2]; 4] = [[-1, 1], [1, 1], [1, -1], [-1, -1]];
for [dx, dy] in bishop_offsets {
for distance in 1..8 {
let x = start[0] as isize + distance as isize * dx;
let y = start[1] as isize + distance as isize * dy;
if (0..8).contains(&x) && (0..8).contains(&y) {
if board[y as usize][x as usize].is_none() {
options.push([x as usize, y as usize]);
} else {
if is_enemy_piece_at(board, start, [x as usize, y as usize]) {
options.push([x as usize, y as usize]);
}
break;
}
} else { break; }
}
}
}
Bicie w przelocie to jedna z najczęściej pomijanych zasad w amatorskich silnikach. Tutaj pole przelotowe jest dynamicznie śledzone w zmiennej en_passant: Option<[usize; 2]>.
Gdy pionek wykona podwójny krok ze swojej pozycji wyjściowej, pole pośrednie staje się celem en passant na dokładnie 1 turę przeciwnika.
Jeśli przeciwnik zdecyduje się na bicie, pionek atakujący wchodzi na puste pole za pionkiem wroga, a zbity pionek zostaje usunięty z planszy: board[position[1]][direction[0]] = None.
// Sprawdzenie czy dany ruch piona to bicie w przelocie:
let is_en_passant = figure.ty == Type::Pawn
&& en_passant == Some(direction)
&& position[0] != direction[0]
&& board[direction[1]][direction[0]].is_none();
if is_en_passant {
// Usunięcie zbijanego piona z linii przelotu:
board[position[1]][direction[0]] = None;
}
board[direction[1]][direction[0]] = board[position[1]][position[0]];
board[position[1]][position[0]] = None;
// Rejestracja pola En Passant po podwójnym kroku piona:
en_passant = if figure.ty == Type::Pawn && position[1].abs_diff(direction[1]) == 2 {
Some([position[0], (position[1] + direction[1]) / 2])
} else {
None
};
Zgodnie z oficjalnymi przepisami FIDE, roszada nie jest dozwolona nie tylko wtedy, gdy król jest szachowany, ale również wtedy, gdy król przechodziłby przez pole atakowane przez przeciwnika!
W module chess_rules.rs zaimplementowano ten warunek poprzez wirtualną symulację planszy pośredniej (transit) oraz docelowej (destination), sprawdzając !is_king_in_check(&transit, side) && !is_king_in_check(&destination, side).
Prawa do roszady śledzone są w tablicy castle: [bool; 4] i bezpowrotnie odbierane w momencie wykonania ruchu królem lub wieżą.
// Krótka roszada: król (e -> g), wieża (h -> f)
if castle[rights]
&& board[home][5].is_none()
&& board[home][6].is_none()
&& board[home][7] == Some(Figure { color: figure.color, ty: Type::Rook })
{
let mut transit = *board;
transit[home][4] = None;
transit[home][5] = Some(figure);
let mut destination = *board;
destination[home][4] = None;
destination[home][6] = Some(figure);
let side = if figure.color == Color::White { 0 } else { 1 };
// Król NIE MOŻE przejść przez szach ani wejść w szach:
if !is_king_in_check(&transit, side) && !is_king_in_check(&destination, side) {
options.push([6, home]);
}
}
Wykrywanie szacha (is_king_in_check) wykorzystuje kierunkowy raycasting z tablicą blokad blocked_directions: [bool; 8].
Gdy promień napotka figurę, linia zostaje zablokowana, a silnik sprawdza, czy bierka wroga jest hetmanem, gońcem, wieżą bądź pionem zagrażającym królowi.
Dodatkowo, każdy wykonany ruch jest weryfikowany — jeżeli gracz zostawił swojego króla w szachu, następuje natychmiastowy rollback planszy (board = board_save; println!("Illegal move");).
Funkcja has_any_legal_moves symuluje każdą dostępną odpowiedź — brak odpowiedzi przy szachu oznacza MAT, a brak odpowiedzi bez szacha to PAT (stalemate)!
// Rollback nielegalnego ruchu (gracz nie może wejść pod szach):
if is_king_in_check(&board, turn) {
board = board_save;
println!("Illegal move");
} else {
promotion(&mut board, turn);
update_castling_rights(&mut castle, figure, position, direction);
turn += 1;
// Sprawdzenie warunków końca partii:
if is_king_in_check(&board, turn) && !has_any_legal_moves(&board, turn, castle, en_passant) {
println!("{:?} player wins", color_for_turn(turn - 1));
break 'game;
} else if !is_king_in_check(&board, turn) && !has_any_legal_moves(&board, turn, castle, en_passant) {
println!("stalemate");
break 'game;
}
}
Gra w pełni rozumie międzynarodową notację szachową, np. e2 e4 lub g1 f3.
Funkcja is_valid_square_notation weryfikuje składnię literowo-cyfrową (kolumny a..=h i rzędy 1..=8),
a parse_square zamienia je w indeksy macierzy [usize; 2].
Po dotarciu piona na 8. linię (dla białych) lub 1. linię (dla czarnych), funkcja promotion wstrzymuje ruch i pyta gracza w terminalu o wybór figury (Hetman, Wieża, Goniec lub Skoczek).
// Parsowanie notacji szachowej ("e2" -> [4, 1]):
pub fn parse_square(code: &str) -> [usize; 2] {
let letters: Vec<&str> = code.split("").filter(|s| !s.is_empty()).collect();
let mut first: usize = 0;
for i in 0..8 {
let arr = ["a", "b", "c", "d", "e", "f", "g", "h"];
if letters[0] == arr[i] {
first = i;
}
}
[first, letters[1].parse::<usize>().unwrap() - 1]
}
// Promocja piona na ostatniej linii:
pub fn promotion(board: &mut [[Option<Figure>; 8]; 8], turn: i32) {
let current_color = color_for_turn(turn);
let promotion_row = match current_color {
Color::White => 7,
Color::Black => 0,
};
for col in 0..8 {
if let Some(piece) = board[promotion_row][col] && piece.ty == Type::Pawn {
println!("Promote pawn at column {} (Knight, Bishop, Queen, Rook):", col + 1);
// Odczyt ze standardowego wejścia io::stdin().read_line(...)
}
}
}
Czego nauczyło mnie pisanie szachów w Rustcie?
Pisanie silnika szachowego to idealny poligon doświadczalny dla systemu własności (Ownership) i pożyczania (Borrowing) w Rustcie.
Symulacja ruchów na klonach planszy (let mut simulation = *board;) wymagała precyzyjnego zarządzania referencjami do tablic 8x8.
Największym wyzwaniem logicznym okazało się bicie w przelocie i roszada, ale dzięki kompilatorowi Rusta wyeliminowane zostały jakiekolwiek błędy pamięciowe czy wycieki.
- Pattern matching: Błyskawiczne dopasowywanie rodzajów figur i kolorów.
- Klonowanie bez GC: Kopiowanie 64-elementowej tablicy na stosie w mikrosekundy.
- Zero zależności: Cały projekt działa na czystej bibliotece standardowej.
[package]
name = "chess"
version = "0.1.0"
edition = "2024"
# Zero zewnętrznych bibliotek - czysty Rust!
[dependencies]
# Kompilacja i uruchomienie:
# > cargo run --bin chess
# > cargo build --release