use sekirei_core::{
board::Board, movegen::generate_legal_moves, mv::Move, piece::PieceKind, square::Square,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GameResult {
BlackWin,
WhiteWin,
Draw,
Unknown,
}
#[derive(Debug)]
pub struct CsaGame {
pub moves: Vec<Move>,
pub result: GameResult,
pub black_rate: Option<f32>,
pub white_rate: Option<f32>,
}
pub fn parse_csa(text: &str) -> Option<CsaGame> {
let mut moves = Vec::new();
let mut result = GameResult::Unknown;
let mut board = Board::startpos();
let mut black_rate: Option<f32> = None;
let mut white_rate: Option<f32> = None;
for line in text.lines() {
let line = line.trim();
if line.is_empty()
|| line.starts_with('$')
|| line.starts_with('V')
|| line.starts_with('N')
|| line.starts_with('T')
{
continue;
}
if line.starts_with("'black_rate:") {
black_rate = line.rsplit(':').next().and_then(|s| s.parse().ok());
continue;
}
if line.starts_with("'white_rate:") {
white_rate = line.rsplit(':').next().and_then(|s| s.parse().ok());
continue;
}
if line.starts_with('\'') {
continue;
}
if line.starts_with('P') {
continue;
}
if line == "+" || line == "-" {
continue;
}
if line.starts_with('%') {
result = match line {
"%TORYO" => {
if board.side_to_move == sekirei_core::color::Color::Black {
GameResult::WhiteWin
} else {
GameResult::BlackWin
}
}
"%TSUMI" => {
if board.side_to_move == sekirei_core::color::Color::Black {
GameResult::WhiteWin } else {
GameResult::BlackWin
}
}
"%KACHI" => {
if board.side_to_move == sekirei_core::color::Color::Black {
GameResult::WhiteWin
} else {
GameResult::BlackWin
}
}
"%JISHOGI" | "%SENNICHITE" => GameResult::Draw,
_ => GameResult::Unknown, };
break;
}
if (line.starts_with('+') || line.starts_with('-')) && line.len() >= 7 {
let bytes = line.as_bytes();
let from_file = bytes[1] - b'0';
let from_rank = bytes[2] - b'0';
let to_file = bytes[3] - b'0';
let to_rank = bytes[4] - b'0';
let piece_str = &line[5..7];
let to_sq = csa_square(to_file, to_rank)?;
let kind = csa_piece(piece_str)?;
let from = if from_file == 0 && from_rank == 0 {
None } else {
Some(csa_square(from_file, from_rank)?)
};
let m = find_legal_move(&mut board, from, to_sq, kind)?;
board.do_move(m);
moves.push(m);
}
}
if moves.is_empty() {
return None;
}
Some(CsaGame {
moves,
result,
black_rate,
white_rate,
})
}
fn csa_square(file: u8, rank: u8) -> Option<Square> {
if file == 0 || file > 9 || rank == 0 || rank > 9 {
return None;
}
Some(Square::from_fr(9 - file, rank - 1))
}
fn csa_piece(s: &str) -> Option<PieceKind> {
Some(match s {
"FU" => PieceKind::Fu,
"KY" => PieceKind::Kyou,
"KE" => PieceKind::Kei,
"GI" => PieceKind::Gin,
"KI" => PieceKind::Kin,
"KA" => PieceKind::Kaku,
"HI" => PieceKind::Hisha,
"OU" => PieceKind::Ou,
"TO" => PieceKind::Tokin,
"NY" => PieceKind::Narikyo,
"NK" => PieceKind::Narikei,
"NG" => PieceKind::Narigin,
"UM" => PieceKind::Uma,
"RY" => PieceKind::Ryu,
_ => return None,
})
}
fn find_legal_move(
board: &mut Board,
from: Option<Square>,
to: Square,
kind_after: PieceKind,
) -> Option<Move> {
let legals = generate_legal_moves(board);
legals.into_iter().find(|m| {
if m.from != from || m.to != to {
return false;
}
let result_kind = if m.promote {
m.piece_kind.promoted()
} else {
m.piece_kind
};
result_kind == kind_after
})
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_CSA: &str = "\
V2.2
N+TestBlack
N-TestWhite
$EVENT:test
$START_TIME:2024/01/01 00:00:00
PI
+
+7776FU
T1
-3334FU
T1
%TORYO
";
#[test]
fn parse_two_moves() {
let game = parse_csa(SAMPLE_CSA).expect("parse failed");
assert_eq!(game.moves.len(), 2);
assert_eq!(game.result, GameResult::WhiteWin);
}
fn sample_with_ending(tag: &str) -> String {
SAMPLE_CSA.replace("%TORYO", tag)
}
#[test]
fn kachi_awards_win_to_the_side_that_just_moved() {
let game = parse_csa(&sample_with_ending("%KACHI")).expect("parse failed");
assert_eq!(game.result, GameResult::WhiteWin);
}
#[test]
fn sennichite_is_a_draw() {
let game = parse_csa(&sample_with_ending("%SENNICHITE")).expect("parse failed");
assert_eq!(game.result, GameResult::Draw);
}
#[test]
fn time_up_is_unknown_not_a_loss() {
let game = parse_csa(&sample_with_ending("%TIME_UP")).expect("parse failed");
assert_eq!(game.result, GameResult::Unknown);
}
}