Mój ostatni projekt

CHESS IN RUST

Mój ostatni projekt — pełnoprawny, konsolowy silnik szachowy napisany w języku Rust (edycja 2024) od zera.

Język i Standard Rust 2024
Zewnętrzne Zależności 0 (Pure Std)
Architektura 5 Modułów
Reguły FIDE 100% Legal
cargo run --bin chess (Console Session)
   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
Pod maską

Architektura i Kluczowe Fragmenty Kodu

01. Model Danych i Stan Planszy
src/piece.rs src/board.rs

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.

chess/src/piece.rs i src/board.rs
// 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
}
02. Ruchy Pseudolegalne i Tablice Offsetów
src/chess_rules.rs Offsets i Raycasting

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.

chess/src/chess_rules.rs (Knight i Raycasting)
// 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; }
        }
    }
}
03. Słynne Bicie w Przelocie (En Passant)
En Passant Logic src/main.rs

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.

chess/src/main.rs (En Passant Execution)
// 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
};
04. Roszada Krótka i Długa z Symulacją Pól Tranzytowych
src/chess_rules.rs FIDE Castling

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żą.

chess/src/chess_rules.rs (Castling Validation)
// 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]);
    }
}
05. Raycasting Szacha, Rollback i Wykrywanie Mata i Pata
Check / Checkmate / Stalemate src/chess_rules.rs

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)!

chess/src/chess_rules.rs i src/main.rs
// 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;
    }
}
06. Promocja Piona i Walidacja Notacji Algebraicznej
src/functions.rs Input Parser

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).

chess/src/functions.rs i src/chess_rules.rs
// 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(...)
        }
    }
}
Rust Logo
Wnioski z projektu

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.
chess/Cargo.toml
[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
← Wróć do strony głównej Więcej o mnie →