xo_core/
lib.rs

1//! # xo-core
2//!
3//! `xo-core` is a fast, reusable, and well-tested Tic-Tac-Toe (Noughts and Crosses) game engine for Rust.
4//! It provides all core logic and an unbeatable Minimax AI, with a simple, public API suitable for embedding
5//! in CLI, GUI, or web apps.
6//!
7//! ## High-Level Summary
8//!
9//! - Core types: [`Player`], [`Cell`], [`GameState`], [`MoveError`]
10//! - [`GameEngine`] struct to manage game state and moves
11//! - Minimax AI: unbeatable computer player with [`GameEngine::get_best_move`]
12//!
13//! ## Example Usage
14//!
15//! ```rust
16//! use xo_core::{GameEngine, Player, Cell, GameState};
17//!
18//! let mut game = GameEngine::new();
19//! // X plays at 0, O at 4, X at 1, O at 5, X at 2 (X wins)
20//! let moves = [0, 4, 1, 5, 2];
21//! for m in moves {
22//!     game.make_move(m).unwrap();
23//! }
24//! assert_eq!(game.check_state(), GameState::Win(Player::X));
25//! ```
26//!
27//! ## Board Layout
28//!
29//! Board cells are indexed as follows:
30//!
31//! ```text
32//!  0 | 1 | 2
33//! ---|---|---
34//!  3 | 4 | 5
35//! ---|---|---
36//!  6 | 7 | 8
37//! ```
38//!
39//! ## Integration
40//!
41//! The engine is detached from I/O and UI, making it easy to use in CLI, GUI, or web applications.
42//!
43//! ## License
44//!
45//! MIT
46
47mod game_engine;
48mod types;
49
50pub use game_engine::GameEngine;
51pub use types::{Cell, GameState, MoveError, Player};
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn x_wins_top_row() {
59        let mut game = GameEngine::new();
60        game.make_move(0).unwrap(); // X
61        game.make_move(3).unwrap(); // O
62        game.make_move(1).unwrap(); // X
63        game.make_move(4).unwrap(); // O
64        game.make_move(2).unwrap(); // X wins
65        assert_eq!(game.check_state(), GameState::Win(Player::X));
66    }
67
68    #[test]
69    fn o_wins_diagonal() {
70        let mut game = GameEngine::new();
71        game.make_move(0).unwrap(); // X
72        game.make_move(2).unwrap(); // O
73        game.make_move(1).unwrap(); // X
74        game.make_move(4).unwrap(); // O
75        game.make_move(3).unwrap(); // X
76        game.make_move(6).unwrap(); // O wins diagonally (2,4,6)
77        assert_eq!(game.check_state(), GameState::Win(Player::O));
78    }
79
80    #[test]
81    fn tie_game() {
82        let mut game = GameEngine::new();
83        let moves = [0, 1, 2, 4, 3, 5, 7, 6, 8];
84        for &i in &moves {
85            game.make_move(i).unwrap();
86        }
87        assert_eq!(game.check_state(), GameState::Tie);
88    }
89
90    #[test]
91    fn cannot_play_out_of_bounds() {
92        let mut game = GameEngine::new();
93        assert_eq!(game.make_move(9), Err(MoveError::OutOfBounds));
94        assert_eq!(game.make_move(99), Err(MoveError::OutOfBounds));
95    }
96
97    #[test]
98    fn cannot_play_on_occupied_cell() {
99        let mut game = GameEngine::new();
100        game.make_move(0).unwrap();
101        assert_eq!(game.make_move(0), Err(MoveError::CellOccupied));
102    }
103
104    #[test]
105    fn board_updates_correctly() {
106        let mut game = GameEngine::new();
107        game.make_move(0).unwrap();
108        let board = game.get_board();
109        assert_eq!(board[0], Cell::X);
110    }
111
112    #[test]
113    fn ai_blocks_win() {
114        let mut game = GameEngine::new();
115        game.make_move(0).unwrap(); // X
116        game.make_move(4).unwrap(); // O
117        game.make_move(1).unwrap(); // X
118                                    // O (AI) should block X at 2
119        assert_eq!(game.get_best_move(), Some(2));
120    }
121
122    #[test]
123    fn ai_chooses_winning_move() {
124        let mut game = GameEngine::new();
125        game.make_move(0).unwrap(); // X
126        game.make_move(4).unwrap(); // O
127        game.make_move(2).unwrap(); // X
128        game.make_move(1).unwrap(); // O
129        game.make_move(3).unwrap(); // X
130                                    // O can win by playing at 7
131        assert_eq!(game.get_best_move(), Some(7));
132    }
133}