Skip to main content

taikyokushogi/
lib.rs

1//! # Taikyoku Shogi Engine
2//!
3//! A complete engine for [Taikyoku Shogi](https://en.wikipedia.org/wiki/Taikyoku_shogi),
4//! the largest known variant of shogi (Japanese chess).
5//!
6//! - **36 x 36** board (1,296 squares)
7//! - **804 pieces** (402 per side)
8//! - **209 piece types** with distinct movement patterns
9//!
10//! ## Quick Start
11//!
12//! ```rust
13//! use taikyokushogi::Board;
14//!
15//! let mut board = Board::initial();
16//! assert_eq!(board.piece_count(taikyokushogi::Color::Black), 402);
17//!
18//! let moves = board.legal_moves();
19//! println!("{} legal moves from starting position", moves.len());
20//!
21//! // Apply the first move
22//! board.apply(&moves[0]);
23//! println!("Score: {}", board.material_score());
24//!
25//! // Undo
26//! board.undo();
27//! ```
28//!
29//! ## Search
30//!
31//! ```rust,no_run
32//! use taikyokushogi::Board;
33//!
34//! let mut board = Board::initial();
35//! let result = board.search(2, 5000); // depth 2, 5s time limit
36//! if let Some(mv) = result.best_move {
37//!     println!("Best move: {}, score: {}", mv, result.score);
38//! }
39//! ```
40
41mod types;
42mod pieces;
43mod board;
44mod movegen;
45mod eval;
46mod search;
47
48#[cfg(feature = "python")]
49mod python;
50
51// ============================================================
52// Public re-exports — the user-facing API
53// ============================================================
54
55/// Board size (36).
56pub const BOARD_SIZE: usize = types::BOARD_SIZE;
57
58/// Number of squares (1,296).
59pub const NUM_SQUARES: usize = types::NUM_SQUARES;
60
61/// Side / player color.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub enum Color {
64    Black = 0,
65    White = 1,
66}
67
68impl Color {
69    /// The other color.
70    pub fn opponent(self) -> Self {
71        match self {
72            Color::Black => Color::White,
73            Color::White => Color::Black,
74        }
75    }
76
77    pub(crate) fn raw(self) -> u8 {
78        self as u8
79    }
80
81    pub(crate) fn from_raw(v: u8) -> Self {
82        if v == 0 { Color::Black } else { Color::White }
83    }
84}
85
86impl std::fmt::Display for Color {
87    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
88        match self {
89            Color::Black => write!(f, "Black"),
90            Color::White => write!(f, "White"),
91        }
92    }
93}
94
95/// Result of a finished game.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum GameResult {
98    BlackWins,
99    WhiteWins,
100    Draw,
101}
102
103impl std::fmt::Display for GameResult {
104    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
105        match self {
106            GameResult::BlackWins => write!(f, "Black wins"),
107            GameResult::WhiteWins => write!(f, "White wins"),
108            GameResult::Draw => write!(f, "Draw"),
109        }
110    }
111}
112
113/// A square on the 36x36 board, represented as `(row, col)`.
114///
115/// - Row 0 = top of the board (White's back rank)
116/// - Row 35 = bottom (Black's back rank)
117/// - Col 0 = left, Col 35 = right
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119pub struct Square {
120    pub row: usize,
121    pub col: usize,
122}
123
124impl Square {
125    pub fn new(row: usize, col: usize) -> Self {
126        Square { row, col }
127    }
128
129    pub(crate) fn from_index(idx: usize) -> Self {
130        Square { row: idx / BOARD_SIZE, col: idx % BOARD_SIZE }
131    }
132
133    /// Convert to flat index (row * 36 + col).
134    pub fn index(&self) -> usize {
135        self.row * BOARD_SIZE + self.col
136    }
137}
138
139impl std::fmt::Display for Square {
140    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
141        write!(f, "({},{})", self.row, self.col)
142    }
143}
144
145/// A piece on the board.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct Piece {
148    /// Internal piece type ID.
149    pub(crate) type_id: u16,
150    /// Color of this piece.
151    pub color: Color,
152}
153
154impl Piece {
155    /// Abbreviation (e.g., `"K"`, `"CP"`, `"GG"`).
156    pub fn abbrev(&self) -> &'static str {
157        pieces::abbrev(self.type_id)
158    }
159
160    /// Full English name.
161    pub fn name(&self) -> &'static str {
162        pieces::name(self.type_id)
163    }
164
165    /// Material value.
166    pub fn value(&self) -> i32 {
167        pieces::value(self.type_id)
168    }
169
170    /// Whether this is a royal piece (King or Crown Prince).
171    pub fn is_royal(&self) -> bool {
172        pieces::is_royal(self.type_id)
173    }
174
175    /// What this piece promotes to, if anything.
176    pub fn promotes_to(&self) -> Option<&'static str> {
177        pieces::promotes_to(self.type_id).map(|pt| pieces::abbrev(pt))
178    }
179}
180
181impl std::fmt::Display for Piece {
182    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
183        let prefix = if self.color == Color::White { 'v' } else { '^' };
184        write!(f, "{}{}", prefix, self.abbrev())
185    }
186}
187
188/// A move in the game.
189#[derive(Debug, Clone)]
190pub struct Move {
191    inner: types::Move,
192}
193
194impl Move {
195    /// Source square.
196    pub fn from(&self) -> Square {
197        Square::from_index(self.inner.from_sq as usize)
198    }
199
200    /// Destination square.
201    pub fn to(&self) -> Square {
202        Square::from_index(self.inner.to_sq as usize)
203    }
204
205    /// Whether this move promotes the piece.
206    pub fn is_promotion(&self) -> bool {
207        self.inner.promotion
208    }
209
210    /// Whether this is an igui (capture without moving).
211    pub fn is_igui(&self) -> bool {
212        self.inner.is_igui
213    }
214
215    /// The captured piece's abbreviation, if any.
216    pub fn captured(&self) -> Option<&'static str> {
217        if self.inner.captured_piece != 0 {
218            Some(pieces::abbrev(self.inner.captured_piece))
219        } else {
220            None
221        }
222    }
223
224    /// Access the internal move representation.
225    pub fn raw(&self) -> &types::Move {
226        &self.inner
227    }
228}
229
230impl std::fmt::Display for Move {
231    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
232        write!(f, "{}->{}", self.from(), self.to())?;
233        if self.is_promotion() { write!(f, "+")?; }
234        Ok(())
235    }
236}
237
238/// Result of a search.
239pub struct SearchResult {
240    /// Best move found, if any.
241    pub best_move: Option<Move>,
242    /// Evaluation score (positive = good for side to move).
243    pub score: i32,
244    /// Number of nodes searched.
245    pub nodes: u64,
246    /// Wall-clock time in milliseconds.
247    pub time_ms: u64,
248}
249
250/// Information about a piece type.
251pub struct PieceInfo {
252    pub abbrev: &'static str,
253    pub name: &'static str,
254    pub value: i32,
255    pub promotes_to: Option<&'static str>,
256    pub slide_directions: usize,
257    pub jump_destinations: usize,
258    pub has_hook: bool,
259    pub area_steps: u8,
260    pub has_range_capture: bool,
261    pub has_igui: bool,
262}
263
264/// Look up information about a piece type by abbreviation.
265///
266/// ```rust
267/// let info = taikyokushogi::piece_info("K").unwrap();
268/// assert_eq!(info.name, "King");
269/// assert!(info.value > 0);
270/// ```
271pub fn piece_info(abbrev: &str) -> Option<PieceInfo> {
272    let pt = pieces::find_by_abbrev(abbrev)?;
273    let mv = pieces::movement(pt);
274    let promo = pieces::promotes_to(pt).map(|p| pieces::abbrev(p));
275    Some(PieceInfo {
276        abbrev: pieces::abbrev(pt),
277        name: pieces::name(pt),
278        value: pieces::value(pt),
279        promotes_to: promo,
280        slide_directions: mv.slides.len(),
281        jump_destinations: mv.jumps.len(),
282        has_hook: mv.hook.is_some(),
283        area_steps: mv.area,
284        has_range_capture: !mv.range_capture.is_empty(),
285        has_igui: mv.igui,
286    })
287}
288
289/// Total number of defined piece types (base + promoted).
290pub fn num_piece_types() -> usize {
291    pieces::num_piece_types()
292}
293
294// ============================================================
295// Board — the main game interface
296// ============================================================
297
298/// The Taikyoku Shogi board.
299///
300/// This is the primary type for interacting with the engine.
301/// Create one with [`Board::initial()`] for the starting position,
302/// or [`Board::empty()`] for an empty board.
303pub struct Board {
304    inner: board::Board,
305}
306
307impl Board {
308    /// Create an empty board.
309    pub fn empty() -> Self {
310        Board { inner: board::Board::new() }
311    }
312
313    /// Create a board with the standard initial position (804 pieces).
314    ///
315    /// ```rust
316    /// let board = taikyokushogi::Board::initial();
317    /// assert_eq!(board.piece_count(taikyokushogi::Color::Black), 402);
318    /// assert_eq!(board.piece_count(taikyokushogi::Color::White), 402);
319    /// ```
320    pub fn initial() -> Self {
321        let mut b = board::Board::new();
322        b.setup_initial();
323        Board { inner: b }
324    }
325
326    /// Whose turn is it?
327    pub fn side_to_move(&self) -> Color {
328        Color::from_raw(self.inner.side_to_move)
329    }
330
331    /// Current full-move number (starts at 1, increments after White moves).
332    pub fn move_number(&self) -> u32 {
333        self.inner.move_number
334    }
335
336    /// Get the piece at `(row, col)`, or `None` if empty.
337    pub fn get(&self, row: usize, col: usize) -> Option<Piece> {
338        let sq = types::sq_index(row, col);
339        let cell = self.inner.cells[sq];
340        if cell == types::EMPTY_CELL {
341            None
342        } else {
343            Some(Piece {
344                type_id: types::cell_piece(cell),
345                color: Color::from_raw(types::cell_color(cell)),
346            })
347        }
348    }
349
350    /// Number of pieces for the given color.
351    pub fn piece_count(&self, color: Color) -> usize {
352        self.inner.piece_count[color.raw() as usize]
353    }
354
355    /// Material score from Black's perspective.
356    /// Positive = Black has more material.
357    pub fn material_score(&self) -> i32 {
358        eval::material_score(&self.inner)
359    }
360
361    /// Evaluate the position from the side to move's perspective.
362    pub fn evaluate(&self) -> i32 {
363        eval::evaluate(&self.inner)
364    }
365
366    /// Check if the game is over.
367    pub fn game_result(&self) -> Option<GameResult> {
368        self.inner.game_result().map(|r| match r {
369            types::GameResult::BlackWins => GameResult::BlackWins,
370            types::GameResult::WhiteWins => GameResult::WhiteWins,
371            types::GameResult::Draw => GameResult::Draw,
372        })
373    }
374
375    /// Generate all legal moves for the side to move.
376    pub fn legal_moves(&self) -> Vec<Move> {
377        movegen::generate_legal_moves(&self.inner)
378            .into_iter()
379            .map(|m| Move { inner: m })
380            .collect()
381    }
382
383    /// Apply a move to the board.
384    pub fn apply(&mut self, mv: &Move) {
385        self.inner.apply_move(&mv.inner);
386    }
387
388    /// Apply a move specified by coordinates. Returns `true` if a matching legal
389    /// move was found and applied, `false` otherwise.
390    ///
391    /// ```rust,no_run
392    /// let mut board = taikyokushogi::Board::initial();
393    /// board.apply_by_coord(25, 0, 24, 0, false); // move pawn forward
394    /// ```
395    pub fn apply_by_coord(&mut self, from_row: usize, from_col: usize,
396                          to_row: usize, to_col: usize, promotion: bool) -> bool {
397        let from_sq = types::sq_index(from_row, from_col) as u16;
398        let to_sq = types::sq_index(to_row, to_col) as u16;
399        let moves = movegen::generate_legal_moves(&self.inner);
400        for m in &moves {
401            if m.from_sq == from_sq && m.to_sq == to_sq && m.promotion == promotion {
402                self.inner.apply_move(m);
403                return true;
404            }
405        }
406        false
407    }
408
409    /// Undo the last move. Returns `false` if there is nothing to undo.
410    pub fn undo(&mut self) -> bool {
411        self.inner.undo_move()
412    }
413
414    /// Pick a uniformly random legal move, or `None` if no moves exist.
415    pub fn random_move(&self) -> Option<Move> {
416        let moves = movegen::generate_legal_moves(&self.inner);
417        if moves.is_empty() { return None; }
418        use rand::Rng;
419        let idx = rand::thread_rng().gen_range(0..moves.len());
420        Some(Move { inner: moves.into_iter().nth(idx).unwrap() })
421    }
422
423    /// Run an alpha-beta search.
424    ///
425    /// - `depth`: search depth in plies
426    /// - `time_limit_ms`: wall-clock time limit in milliseconds (0 = unlimited)
427    pub fn search(&mut self, depth: u32, time_limit_ms: u64) -> SearchResult {
428        let r = search::search(&mut self.inner, depth, time_limit_ms);
429        SearchResult {
430            best_move: r.best_move.map(|m| Move { inner: m }),
431            score: r.score,
432            nodes: r.nodes,
433            time_ms: r.time_ms,
434        }
435    }
436
437    /// Render the board as a text string.
438    pub fn display(&self) -> String {
439        self.inner.display()
440    }
441
442    /// Iterate over all pieces of a given color.
443    pub fn pieces(&self, color: Color) -> Vec<(Square, Piece)> {
444        let c = color.raw() as usize;
445        let mut result = Vec::new();
446        for i in 0..self.inner.piece_list_len[c] {
447            let sq = self.inner.piece_list[c][i];
448            if sq == types::INVALID_SQ { continue; }
449            let cell = self.inner.cells[sq as usize];
450            if cell == types::EMPTY_CELL { continue; }
451            result.push((
452                Square::from_index(sq as usize),
453                Piece {
454                    type_id: types::cell_piece(cell),
455                    color,
456                },
457            ));
458        }
459        result
460    }
461}
462
463impl Clone for Board {
464    fn clone(&self) -> Self {
465        Board { inner: self.inner.clone() }
466    }
467}
468
469impl std::fmt::Display for Board {
470    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
471        write!(f, "{}", self.display())
472    }
473}
474
475impl std::fmt::Debug for Board {
476    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
477        f.debug_struct("Board")
478            .field("side_to_move", &self.side_to_move())
479            .field("move_number", &self.move_number())
480            .field("black_pieces", &self.piece_count(Color::Black))
481            .field("white_pieces", &self.piece_count(Color::White))
482            .finish()
483    }
484}
485
486// ============================================================
487// PyO3 module entry point (only with "python" feature)
488// ============================================================
489
490#[cfg(feature = "python")]
491use pyo3::prelude::*;
492
493#[cfg(feature = "python")]
494#[pymodule]
495fn taikyokushogi(m: &Bound<'_, PyModule>) -> PyResult<()> {
496    python::register(m)
497}