use std::env;
use std::fmt::Write;
use std::fs;
use std::path::Path;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
let dest_path = Path::new(&out_dir);
generate_square_tables(dest_path);
generate_attack_tables(dest_path);
generate_policy_label_tables(dest_path);
}
fn generate_square_tables(out_dir: &Path) {
let mut output = String::new();
output.push_str(
"// Auto-generated by build.rs - DO NOT EDIT
",
);
output.push_str(
"/// マスから筋への変換テーブル (82要素: 0..81 + `SQ_NONE`)
",
);
output.push_str(
"pub const SQUARE_TO_FILE: [File; 82] = [
",
);
for sq in 0..81 {
let file = sq / 9;
if sq % 9 == 0 {
output.push_str(" ");
}
let _ = write!(output, "File::new({file}), ");
if sq % 9 == 8 {
let _ = writeln!(output, "// SQ_{}1 - SQ_{}9", 9 - file, 9 - file);
}
}
output.push_str(
" File::new(-1), // SQ_NONE
",
);
output.push_str(
"];
",
);
output.push_str(
"/// マスから段への変換テーブル (82要素: 0..81 + `SQ_NONE`)
",
);
output.push_str(
"pub const SQUARE_TO_RANK: [Rank; 82] = [
",
);
for sq in 0..81 {
let rank = sq % 9;
if sq % 9 == 0 {
output.push_str(" ");
}
let _ = write!(output, "Rank::new({rank}), ");
if sq % 9 == 8 {
let _ = writeln!(output, "// SQ_{}1 - SQ_{}9", 9 - sq / 9, 9 - sq / 9);
}
}
output.push_str(
" Rank::new(-1), // SQ_NONE
",
);
output.push_str(
"];
",
);
let path = out_dir.join("square_tables.rs");
fs::write(path, output).expect("Failed to write square_tables.rs");
}
fn generate_attack_tables(out_dir: &Path) {
let mut output = String::new();
output.push_str(
"// Auto-generated attack tables - DO NOT EDIT\n\
\n\
use crate::board::{Bitboard, Bitboard256};\n\
use crate::types::SquareTable;\n\
\n",
);
output.push_str(&generate_non_slider_attacks());
output.push_str(&generate_slider_beams());
output.push_str(&generate_qugiy_step_attacks());
output.push_str(&generate_qugiy_rook_masks());
output.push_str(&generate_qugiy_bishop_masks());
output.push_str(&generate_check_candidate_table());
let path = out_dir.join("attack_tables.rs");
fs::write(path, output).expect("Failed to write attack_tables.rs");
}
const POLICY_LABEL_SQUARES: usize = 81;
const POLICY_LABEL_CLASS_COUNT: usize = 27;
const POLICY_LABEL_COUNT: usize = POLICY_LABEL_SQUARES * POLICY_LABEL_CLASS_COUNT;
const COMPACT_POLICY_LABEL_COUNT: usize = 1496;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum PolicyPieceType {
Pawn,
Lance,
Knight,
Silver,
Bishop,
Rook,
Gold,
King,
ProPawn,
ProLance,
ProKnight,
ProSilver,
Horse,
Dragon,
}
impl PolicyPieceType {
const ALL: [Self; 14] = [
Self::Pawn,
Self::Lance,
Self::Knight,
Self::Silver,
Self::Bishop,
Self::Rook,
Self::Gold,
Self::King,
Self::ProPawn,
Self::ProLance,
Self::ProKnight,
Self::ProSilver,
Self::Horse,
Self::Dragon,
];
const fn is_promotable(self) -> bool {
matches!(
self,
Self::Pawn | Self::Lance | Self::Knight | Self::Silver | Self::Bishop | Self::Rook
)
}
}
fn generate_policy_label_tables(out_dir: &Path) {
let compact_to_move_label = compute_policy_structural_labels();
assert_eq!(
compact_to_move_label.len(),
COMPACT_POLICY_LABEL_COUNT,
"unexpected compact policy label count"
);
let mut move_label_to_compact = vec![None; POLICY_LABEL_COUNT];
for (compact, &move_label) in compact_to_move_label.iter().enumerate() {
move_label_to_compact[usize::from(move_label)] =
Some(u16::try_from(compact).expect("compact label index fits in u16"));
}
let mut output = String::new();
output.push_str("// Auto-generated by build.rs - DO NOT EDIT\n\n");
let _ = writeln!(
output,
"pub const COMPACT_TO_MOVE_LABEL: [u16; {COMPACT_POLICY_LABEL_COUNT}] = ["
);
for (idx, label) in compact_to_move_label.iter().enumerate() {
if idx % 16 == 0 {
output.push_str(" ");
}
let _ = write!(output, "{label}, ");
if idx % 16 == 15 {
output.push('\n');
}
}
if !compact_to_move_label.len().is_multiple_of(16) {
output.push('\n');
}
output.push_str("];\n\n");
let _ = writeln!(
output,
"pub const MOVE_LABEL_TO_COMPACT: [Option<u16>; {POLICY_LABEL_COUNT}] = ["
);
for (idx, label) in move_label_to_compact.iter().enumerate() {
if idx % 12 == 0 {
output.push_str(" ");
}
match label {
Some(value) => {
let _ = write!(output, "Some({value}), ");
}
None => output.push_str("None, "),
}
if idx % 12 == 11 {
output.push('\n');
}
}
if move_label_to_compact.len() % 12 != 0 {
output.push('\n');
}
output.push_str("];\n");
let path = out_dir.join("policy_label_tables.rs");
fs::write(path, output).expect("Failed to write policy_label_tables.rs");
}
fn compute_policy_structural_labels() -> Vec<u16> {
let mut valid = vec![false; POLICY_LABEL_COUNT];
for piece_type in PolicyPieceType::ALL {
for from in 0..POLICY_LABEL_SQUARES {
add_piece_labels(piece_type, from, &mut valid);
}
}
for to in 0..POLICY_LABEL_SQUARES {
let (_, to_y) = policy_square_xy(to);
if to_y != 0 {
mark_label(20, to, &mut valid);
mark_label(21, to, &mut valid);
}
if to_y > 1 {
mark_label(22, to, &mut valid);
}
mark_label(23, to, &mut valid);
mark_label(24, to, &mut valid);
mark_label(25, to, &mut valid);
mark_label(26, to, &mut valid);
}
valid
.into_iter()
.enumerate()
.filter_map(|(idx, is_valid)| {
is_valid.then_some(u16::try_from(idx).expect("label fits in u16"))
})
.collect()
}
fn add_piece_labels(piece_type: PolicyPieceType, from: usize, valid: &mut [bool]) {
for &(dx, dy) in piece_step_offsets(piece_type) {
let Some(to) = policy_offset_square(from, dx, dy) else {
continue;
};
add_board_move_label(piece_type, from, to, valid);
}
for &(dx, dy) in piece_slide_offsets(piece_type) {
let mut next = policy_offset_square(from, dx, dy);
while let Some(to) = next {
add_board_move_label(piece_type, from, to, valid);
next = policy_offset_square(to, dx, dy);
}
}
}
const fn piece_step_offsets(piece_type: PolicyPieceType) -> &'static [(i8, i8)] {
const PAWN: &[(i8, i8)] = &[(0, -1)];
const KNIGHT: &[(i8, i8)] = &[(-1, -2), (1, -2)];
const SILVER: &[(i8, i8)] = &[(-1, -1), (0, -1), (1, -1), (-1, 1), (1, 1)];
const GOLD_LIKE: &[(i8, i8)] = &[(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (0, 1)];
const KING: &[(i8, i8)] =
&[(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)];
const HORSE_STEPS: &[(i8, i8)] = &[(-1, 0), (1, 0), (0, -1), (0, 1)];
const DRAGON_STEPS: &[(i8, i8)] = &[(-1, -1), (1, -1), (-1, 1), (1, 1)];
match piece_type {
PolicyPieceType::Pawn => PAWN,
PolicyPieceType::Knight => KNIGHT,
PolicyPieceType::Silver => SILVER,
PolicyPieceType::Gold
| PolicyPieceType::ProPawn
| PolicyPieceType::ProLance
| PolicyPieceType::ProKnight
| PolicyPieceType::ProSilver => GOLD_LIKE,
PolicyPieceType::King => KING,
PolicyPieceType::Horse => HORSE_STEPS,
PolicyPieceType::Dragon => DRAGON_STEPS,
PolicyPieceType::Lance | PolicyPieceType::Bishop | PolicyPieceType::Rook => &[],
}
}
const fn piece_slide_offsets(piece_type: PolicyPieceType) -> &'static [(i8, i8)] {
const LANCE: &[(i8, i8)] = &[(0, -1)];
const BISHOP: &[(i8, i8)] = &[(-1, -1), (1, -1), (-1, 1), (1, 1)];
const ROOK: &[(i8, i8)] = &[(-1, 0), (1, 0), (0, -1), (0, 1)];
match piece_type {
PolicyPieceType::Lance => LANCE,
PolicyPieceType::Bishop | PolicyPieceType::Horse => BISHOP,
PolicyPieceType::Rook | PolicyPieceType::Dragon => ROOK,
PolicyPieceType::Pawn
| PolicyPieceType::Knight
| PolicyPieceType::Silver
| PolicyPieceType::Gold
| PolicyPieceType::King
| PolicyPieceType::ProPawn
| PolicyPieceType::ProLance
| PolicyPieceType::ProKnight
| PolicyPieceType::ProSilver => &[],
}
}
fn add_board_move_label(piece_type: PolicyPieceType, from: usize, to: usize, valid: &mut [bool]) {
let move_class = classify_move_label_class(from, to);
if piece_type.is_promotable() {
if is_mandatory_promotion(piece_type, to) {
mark_label(move_class + 10, to, valid);
return;
}
mark_label(move_class, to, valid);
if is_promotion_move(from, to) {
mark_label(move_class + 10, to, valid);
}
return;
}
mark_label(move_class, to, valid);
}
fn policy_offset_square(from: usize, dx: i8, dy: i8) -> Option<usize> {
let (from_x, from_y) = policy_square_xy(from);
let to_x = from_x + dx;
let to_y = from_y + dy;
if !(0..=8).contains(&to_x) || !(0..=8).contains(&to_y) {
return None;
}
let to_x = usize::try_from(to_x).ok()?;
let to_y = usize::try_from(to_y).ok()?;
Some(to_x * 9 + to_y)
}
fn policy_square_xy(square: usize) -> (i8, i8) {
let x = i8::try_from(square / 9).expect("file fits in i8");
let y = i8::try_from(square % 9).expect("rank fits in i8");
(x, y)
}
fn classify_move_label_class(from: usize, to: usize) -> usize {
let (from_x, from_y) = policy_square_xy(from);
let (to_x, to_y) = policy_square_xy(to);
let dir_x = from_x - to_x;
let dir_y = to_y - from_y;
if dir_y < 0 && dir_x == 0 {
0
} else if dir_y == -2 && dir_x == -1 {
8
} else if dir_y == -2 && dir_x == 1 {
9
} else if dir_y < 0 && dir_x < 0 {
1
} else if dir_y < 0 && dir_x > 0 {
2
} else if dir_y == 0 && dir_x < 0 {
3
} else if dir_y == 0 && dir_x > 0 {
4
} else if dir_y > 0 && dir_x == 0 {
5
} else if dir_y > 0 && dir_x < 0 {
6
} else {
7
}
}
fn is_promotion_move(from: usize, to: usize) -> bool {
is_promotion_zone_square(from) || is_promotion_zone_square(to)
}
fn is_promotion_zone_square(square: usize) -> bool {
let (_, y) = policy_square_xy(square);
y <= 2
}
fn is_mandatory_promotion(piece_type: PolicyPieceType, to: usize) -> bool {
let (_, to_y) = policy_square_xy(to);
match piece_type {
PolicyPieceType::Pawn | PolicyPieceType::Lance => to_y == 0,
PolicyPieceType::Knight => to_y <= 1,
PolicyPieceType::Silver
| PolicyPieceType::Bishop
| PolicyPieceType::Rook
| PolicyPieceType::Gold
| PolicyPieceType::King
| PolicyPieceType::ProPawn
| PolicyPieceType::ProLance
| PolicyPieceType::ProKnight
| PolicyPieceType::ProSilver
| PolicyPieceType::Horse
| PolicyPieceType::Dragon => false,
}
}
fn mark_label(class_idx: usize, to: usize, valid: &mut [bool]) {
valid[class_idx * POLICY_LABEL_SQUARES + to] = true;
}
fn generate_check_candidate_table() -> String {
let mut output = String::new();
output.push_str(
"/// 王手候補テーブル \\[king_sq\\]\\[piece_index\\]\\[color\\]\n\
/// piece_index: 0=PAWN,1=LANCE,2=KNIGHT,3=SILVER,4=GOLD,5=BISHOP,6=ROOK(HORSE)\n\
#[allow(clippy::large_const_arrays)]\n\
pub const CHECK_CANDIDATE_BB: [[[Bitboard; 2]; 7]; 81] = [\n",
);
for ksq in 0..81 {
output.push_str(" [\n");
for pt_idx in 0..7 {
output.push_str(" [\n");
for us in 0..2 {
let bb = check_candidate_bb(ksq, pt_idx, us);
let _ = writeln!(output, " Bitboard::from_packed_bits({bb}u128),");
}
output.push_str(" ],\n");
}
output.push_str(" ],\n");
}
output.push_str("];\n\n");
output
}
#[allow(clippy::too_many_lines)]
fn check_candidate_bb(ksq: i8, pt_idx: usize, us: usize) -> u128 {
let them = us ^ 1;
let enemy_field = enemy_field_mask(us);
let enemy_gold = gold_attacks(them, ksq) & enemy_field;
let mut target = 0u128;
match pt_idx {
0 => {
let mut bb = pawn_attacks(them, ksq);
while let Some(sq) = pop_lsb(&mut bb) {
target |= pawn_attacks(them, sq);
}
let mut bb = enemy_gold;
while let Some(sq) = pop_lsb(&mut bb) {
target |= pawn_attacks(them, sq);
}
target &= !bit_of(ksq);
}
1 => {
target = lance_step_attacks(them, ksq);
if (enemy_field & bit_of(ksq)) != 0 {
let file = ksq / 9;
let rank = ksq % 9;
if file != 0 {
let sq = (file - 1) * 9 + rank;
target |= lance_step_attacks(them, sq);
}
if file != 8 {
let sq = (file + 1) * 9 + rank;
target |= lance_step_attacks(them, sq);
}
}
}
2 => {
let mut bb = knight_attacks(them, ksq) | enemy_gold;
while let Some(sq) = pop_lsb(&mut bb) {
target |= knight_attacks(them, sq);
}
target &= !bit_of(ksq);
}
3 => {
let mut bb = silver_attacks(them, ksq);
while let Some(sq) = pop_lsb(&mut bb) {
target |= silver_attacks(them, sq);
}
let mut bb = enemy_gold;
while let Some(sq) = pop_lsb(&mut bb) {
target |= silver_attacks(them, sq);
}
let mut bb = gold_attacks(them, ksq);
while let Some(sq) = pop_lsb(&mut bb) {
target |= silver_attacks(them, sq) & enemy_field;
}
target &= !bit_of(ksq);
}
4 => {
let mut bb = gold_attacks(them, ksq);
while let Some(sq) = pop_lsb(&mut bb) {
target |= gold_attacks(them, sq);
}
target &= !bit_of(ksq);
}
5 => {
let mut bb = bishop_step_attacks(ksq);
while let Some(sq) = pop_lsb(&mut bb) {
target |= bishop_step_attacks(sq);
}
let mut bb = king_attacks(ksq) & enemy_field;
while let Some(sq) = pop_lsb(&mut bb) {
target |= bishop_step_attacks(sq);
}
let mut bb = king_attacks(ksq);
while let Some(sq) = pop_lsb(&mut bb) {
target |= bishop_step_attacks(sq) & enemy_field;
}
target &= !bit_of(ksq);
}
6 => {
let mut bb = horse_attacks(ksq);
while let Some(sq) = pop_lsb(&mut bb) {
target |= horse_attacks(sq);
}
target &= !bit_of(ksq);
}
_ => {}
}
target
}
fn bit_of(sq: i8) -> u128 {
let shift = u32::try_from(sq).expect("square index is non-negative");
1u128 << shift
}
fn pop_lsb(bb: &mut u128) -> Option<i8> {
if *bb == 0 {
return None;
}
let idx = bb.trailing_zeros();
*bb &= *bb - 1;
Some(i8::try_from(idx).expect("square index fits in i8"))
}
fn enemy_field_mask(us: usize) -> u128 {
let ranks: [i8; 3] = if us == 0 { [0, 1, 2] } else { [6, 7, 8] };
let mut mask = 0u128;
for rank in ranks {
for file in 0..9 {
let sq = file * 9 + rank;
mask |= bit_of(sq);
}
}
mask
}
fn pawn_attacks(color: usize, sq: i8) -> u128 {
generate_pawn_attack(sq, color)
}
fn knight_attacks(color: usize, sq: i8) -> u128 {
generate_knight_attack(sq, color)
}
fn silver_attacks(color: usize, sq: i8) -> u128 {
generate_silver_attack(sq, color)
}
fn gold_attacks(color: usize, sq: i8) -> u128 {
generate_gold_attack(sq, color)
}
fn king_attacks(sq: i8) -> u128 {
generate_king_attack(sq)
}
fn lance_step_attacks(color: usize, sq: i8) -> u128 {
let (black, white) = generate_lance_beams(sq);
if color == 0 { black } else { white }
}
fn bishop_step_attacks(sq: i8) -> u128 {
let (ne, se, sw, nw) = generate_bishop_beams(sq);
ne | se | sw | nw
}
fn horse_attacks(sq: i8) -> u128 {
bishop_step_attacks(sq) | king_attacks(sq)
}
fn generate_non_slider_attacks() -> String {
let mut output = String::new();
output.push_str("/// 歩の攻撃パターン \\[sq\\]\\[color\\]\n");
output
.push_str("pub const PAWN_ATTACKS: SquareTable<[Bitboard; 2], 81> = SquareTable::new([\n");
for sq in 0..81 {
output.push_str(" [\n");
for color in 0..2 {
let bb = generate_pawn_attack(sq, color);
let _ = writeln!(output, " Bitboard::from_packed_bits({bb}u128),");
}
output.push_str(" ],\n");
}
output.push_str("]);\n\n");
output.push_str("/// 桂馬の攻撃パターン \\[sq\\]\\[color\\]\n");
output.push_str(
"pub const KNIGHT_ATTACKS: SquareTable<[Bitboard; 2], 81> = SquareTable::new([\n",
);
for sq in 0..81 {
output.push_str(" [\n");
for color in 0..2 {
let bb = generate_knight_attack(sq, color);
let _ = writeln!(output, " Bitboard::from_packed_bits({bb}u128),");
}
output.push_str(" ],\n");
}
output.push_str("]);\n\n");
output.push_str("/// 銀の攻撃パターン \\[sq\\]\\[color\\]\n");
output.push_str(
"pub const SILVER_ATTACKS: SquareTable<[Bitboard; 2], 81> = SquareTable::new([\n",
);
for sq in 0..81 {
output.push_str(" [\n");
for color in 0..2 {
let bb = generate_silver_attack(sq, color);
let _ = writeln!(output, " Bitboard::from_packed_bits({bb}u128),");
}
output.push_str(" ],\n");
}
output.push_str("]);\n\n");
output.push_str("/// 金の攻撃パターン \\[sq\\]\\[color\\]\n");
output
.push_str("pub const GOLD_ATTACKS: SquareTable<[Bitboard; 2], 81> = SquareTable::new([\n");
for sq in 0..81 {
output.push_str(" [\n");
for color in 0..2 {
let bb = generate_gold_attack(sq, color);
let _ = writeln!(output, " Bitboard::from_packed_bits({bb}u128),");
}
output.push_str(" ],\n");
}
output.push_str("]);\n\n");
output.push_str("/// 玉の攻撃パターン \\[sq\\]\n");
output.push_str("pub const KING_ATTACKS: SquareTable<Bitboard, 81> = SquareTable::new([\n");
for sq in 0..81 {
let bb = generate_king_attack(sq);
let _ = writeln!(output, " Bitboard::from_packed_bits({bb}u128),");
}
output.push_str("]);\n\n");
output
}
fn generate_pawn_attack(sq: i8, color: usize) -> u128 {
let file = sq / 9;
let rank = sq % 9;
let forward_rank = if color == 0 { rank - 1 } else { rank + 1 };
if !(0..9).contains(&forward_rank) {
return 0; }
let target_sq = file * 9 + forward_rank;
1u128 << target_sq
}
fn generate_knight_attack(sq: i8, color: usize) -> u128 {
let file = sq / 9;
let rank = sq % 9;
let mut attacks = 0u128;
let forward_offset = if color == 0 { -2 } else { 2 };
let target_rank = rank + forward_offset;
if !(0..9).contains(&target_rank) {
return 0;
}
if (0..9).contains(&(file + 1)) {
let target_sq = (file + 1) * 9 + target_rank;
attacks |= 1u128 << target_sq;
}
if (0..9).contains(&(file - 1)) {
let target_sq = (file - 1) * 9 + target_rank;
attacks |= 1u128 << target_sq;
}
attacks
}
fn generate_silver_attack(sq: i8, color: usize) -> u128 {
let file = sq / 9;
let rank = sq % 9;
let mut attacks = 0u128;
let forward = if color == 0 { -1 } else { 1 };
let backward = -forward;
if let Some(target_sq) = add_direction(file, rank, 0, forward) {
attacks |= 1u128 << target_sq;
}
if let Some(target_sq) = add_direction(file, rank, 1, forward) {
attacks |= 1u128 << target_sq;
}
if let Some(target_sq) = add_direction(file, rank, -1, forward) {
attacks |= 1u128 << target_sq;
}
if let Some(target_sq) = add_direction(file, rank, 1, backward) {
attacks |= 1u128 << target_sq;
}
if let Some(target_sq) = add_direction(file, rank, -1, backward) {
attacks |= 1u128 << target_sq;
}
attacks
}
fn generate_gold_attack(sq: i8, color: usize) -> u128 {
let file = sq / 9;
let rank = sq % 9;
let mut attacks = 0u128;
#[allow(clippy::cast_possible_wrap)]
let black_offsets: &[(i8, i8)] = &[
(0, -1), (1, -1), (-1, -1), (1, 0), (-1, 0), (0, 1), ];
let white_offsets: &[(i8, i8)] = &[
(0, 1), (-1, 1), (1, 1), (-1, 0), (1, 0), (0, -1), ];
let offsets = if color == 0 { black_offsets } else { white_offsets };
for &(df, dr) in offsets {
if let Some(target_sq) = add_direction(file, rank, df, dr) {
attacks |= 1u128 << target_sq;
}
}
attacks
}
fn generate_king_attack(sq: i8) -> u128 {
let file = sq / 9;
let rank = sq % 9;
let mut attacks = 0u128;
for df in -1..=1 {
for dr in -1..=1 {
if df == 0 && dr == 0 {
continue; }
if let Some(target_sq) = add_direction(file, rank, df, dr) {
attacks |= 1u128 << target_sq;
}
}
}
attacks
}
fn add_direction(file: i8, rank: i8, df: i8, dr: i8) -> Option<i8> {
let new_file = file + df;
let new_rank = rank + dr;
if (0..9).contains(&new_file) && (0..9).contains(&new_rank) {
Some(new_file * 9 + new_rank)
} else {
None
}
}
fn generate_slider_beams() -> String {
let mut output = String::new();
output.push_str(
r"/// 香車のビーム構造
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LanceBeams {
pub black: Bitboard,
pub white: Bitboard,
}
",
);
output.push_str(
r"/// 角のビーム構造
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BishopBeams {
pub ne: Bitboard,
pub se: Bitboard,
pub sw: Bitboard,
pub nw: Bitboard,
}
",
);
output.push_str(
r"/// 飛車のビーム構造
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RookBeams {
pub n: Bitboard,
pub e: Bitboard,
pub s: Bitboard,
pub w: Bitboard,
}
",
);
output.push_str("/// 香車のビームテーブル \\[sq\\]\n");
output.push_str("pub const LANCE_BEAMS: [LanceBeams; 81] = [\n");
for sq in 0..81 {
let (black_beam, white_beam) = generate_lance_beams(sq);
let _ = writeln!(
output,
" LanceBeams {{ black: Bitboard::from_packed_bits({black_beam}u128), white: Bitboard::from_packed_bits({white_beam}u128) }},"
);
}
output.push_str("];\n\n");
output.push_str("/// 角のビームテーブル \\[sq\\]\n");
output.push_str("pub const BISHOP_BEAMS: [BishopBeams; 81] = [\n");
for sq in 0..81 {
let (ne, se, sw, nw) = generate_bishop_beams(sq);
let _ = writeln!(
output,
" BishopBeams {{ ne: Bitboard::from_packed_bits({ne}u128), se: Bitboard::from_packed_bits({se}u128), sw: Bitboard::from_packed_bits({sw}u128), nw: Bitboard::from_packed_bits({nw}u128) }},"
);
}
output.push_str("];\n\n");
output.push_str("/// 飛車のビームテーブル \\[sq\\]\n");
output.push_str("pub const ROOK_BEAMS: [RookBeams; 81] = [\n");
for sq in 0..81 {
let (n, e, s, w) = generate_rook_beams(sq);
let _ = writeln!(
output,
" RookBeams {{ n: Bitboard::from_packed_bits({n}u128), e: Bitboard::from_packed_bits({e}u128), s: Bitboard::from_packed_bits({s}u128), w: Bitboard::from_packed_bits({w}u128) }},"
);
}
output.push_str("];\n\n");
output
}
fn generate_qugiy_step_attacks() -> String {
let mut output = String::new();
output.push_str(
"/// Qugiyの方向利きステップテーブル \\[dir\\]\\[sq\\]\n\
/// dir index: 0=RU, 1=R, 2=RD, 3=LU, 4=L, 5=LD\n\
pub const QUGIY_STEP_ATTACKS: [[Bitboard; 81]; 6] = [\n",
);
let directions: [(i8, i8); 6] = [
(-1, -1), (-1, 0), (-1, 1), (1, -1), (1, 0), (1, 1), ];
let reverse_dirs = [true, true, true, false, false, false];
for (dir_idx, (df, dr)) in directions.iter().enumerate() {
output.push_str(" [\n");
for sq in 0..81 {
let mut mask = generate_ray_mask(sq, *df, *dr);
if reverse_dirs[dir_idx] {
mask = byte_reverse_u128(mask);
}
let _ = writeln!(output, " Bitboard::from_packed_bits_unchecked({mask}u128),");
}
output.push_str(" ],\n");
}
output.push_str("];\n\n");
output
}
fn generate_qugiy_rook_masks() -> String {
let mut output = String::new();
output.push_str("/// Qugiyの飛車マスクテーブル \\[sq\\]\\[lo/hi\\]\n");
output.push_str("pub const QUGIY_ROOK_MASK: [[Bitboard; 2]; 81] = [\n");
for sq in 0..81 {
let left = generate_ray_parts(sq, 1, 0);
let right = generate_ray_parts(sq, -1, 0);
let right_rev = (right.1.swap_bytes(), right.0.swap_bytes());
let (hi, lo) = unpack_parts(right_rev, left);
let hi = parts_to_raw_u128(hi);
let lo = parts_to_raw_u128(lo);
output.push_str(" [\n");
let _ = writeln!(output, " Bitboard::from_packed_bits_unchecked({lo}u128),");
let _ = writeln!(output, " Bitboard::from_packed_bits_unchecked({hi}u128),");
output.push_str(" ],\n");
}
output.push_str("];\n\n");
output
}
#[allow(clippy::similar_names)]
fn generate_qugiy_bishop_masks() -> String {
let mut output = String::new();
output.push_str("/// Qugiyの角マスクテーブル \\[sq\\]\\[part\\]\n");
output.push_str("pub const QUGIY_BISHOP_MASK: [[Bitboard256; 2]; 81] = [\n");
for sq in 0..81 {
let lu = generate_ray_mask(sq, 1, -1);
let ld = generate_ray_mask(sq, 1, 1);
let ru = byte_reverse_u128(generate_ray_mask(sq, -1, -1));
let rd = byte_reverse_u128(generate_ray_mask(sq, -1, 1));
output.push_str(" [\n");
for part in 0..2 {
let lu_part = extract64(lu, part);
let ru_part = extract64(ru, part);
let ld_part = extract64(ld, part);
let rd_part = extract64(rd, part);
let _ = writeln!(
output,
" Bitboard256::new({lu_part}, {ru_part}, {ld_part}, {rd_part}),"
);
}
output.push_str(" ],\n");
}
output.push_str("];\n\n");
output
}
fn generate_ray_mask(sq: i8, df: i8, dr: i8) -> u128 {
let file = sq / 9;
let rank = sq % 9;
let mut mask = 0u128;
let mut f = file + df;
let mut r = rank + dr;
while (0..9).contains(&f) && (0..9).contains(&r) {
let target_sq = f * 9 + r;
mask |= 1u128 << raw_bit_index(target_sq);
f += df;
r += dr;
}
mask
}
#[allow(clippy::cast_possible_truncation)]
const fn parts_to_raw_u128(parts: (u64, u64)) -> u128 {
(parts.0 as u128) | ((parts.1 as u128) << 64)
}
fn square_parts(sq: i8) -> (u64, u64) {
let file = sq / 9;
let rank = sq % 9;
if file < 7 {
let shift = u32::try_from(file * 9 + rank).expect("square index fits in u32");
(1u64 << shift, 0)
} else {
let shift = u32::try_from((file - 7) * 9 + rank).expect("square index fits in u32");
(0, 1u64 << shift)
}
}
fn generate_ray_parts(sq: i8, df: i8, dr: i8) -> (u64, u64) {
let file = sq / 9;
let rank = sq % 9;
let mut p0 = 0u64;
let mut p1 = 0u64;
let mut f = file + df;
let mut r = rank + dr;
while (0..9).contains(&f) && (0..9).contains(&r) {
let target_sq = f * 9 + r;
let (b0, b1) = square_parts(target_sq);
p0 |= b0;
p1 |= b1;
f += df;
r += dr;
}
(p0, p1)
}
#[allow(clippy::cast_possible_truncation)]
const fn unpack_parts(hi_in: (u64, u64), lo_in: (u64, u64)) -> ((u64, u64), (u64, u64)) {
let hi_out = (lo_in.1, hi_in.1);
let lo_out = (lo_in.0, hi_in.0);
(hi_out, lo_out)
}
#[allow(clippy::cast_possible_truncation)]
const fn extract64(packed_bits: u128, part: usize) -> u64 {
if part == 0 { packed_bits as u64 } else { (packed_bits >> 64) as u64 }
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
const fn raw_bit_index(sq: i8) -> u32 {
if sq >= 63 { (sq as u32) + 1 } else { sq as u32 }
}
#[allow(clippy::cast_possible_truncation)]
const fn byte_reverse_u128(packed_bits: u128) -> u128 {
let low = packed_bits as u64;
let high = (packed_bits >> 64) as u64;
((low.swap_bytes() as u128) << 64) | (high.swap_bytes() as u128)
}
fn generate_lance_beams(sq: i8) -> (u128, u128) {
let file = sq / 9;
let rank = sq % 9;
let mut black_beam = 0u128; let mut white_beam = 0u128;
for r in (0..rank).rev() {
let target_sq = file * 9 + r;
black_beam |= 1u128 << target_sq;
}
for r in (rank + 1)..9 {
let target_sq = file * 9 + r;
white_beam |= 1u128 << target_sq;
}
(black_beam, white_beam)
}
#[allow(clippy::similar_names)]
fn generate_bishop_beams(sq: i8) -> (u128, u128, u128, u128) {
let file = sq / 9;
let rank = sq % 9;
let mut ne_beam = 0u128; let mut se_beam = 0u128; let mut sw_beam = 0u128; let mut nw_beam = 0u128;
let mut f = file + 1;
let mut r = rank - 1;
while (0..9).contains(&f) && (0..9).contains(&r) {
let target_sq = f * 9 + r;
ne_beam |= 1u128 << target_sq;
f += 1;
r -= 1;
}
let mut f = file + 1;
let mut r = rank + 1;
while (0..9).contains(&f) && (0..9).contains(&r) {
let target_sq = f * 9 + r;
se_beam |= 1u128 << target_sq;
f += 1;
r += 1;
}
let mut f = file - 1;
let mut r = rank + 1;
while (0..9).contains(&f) && (0..9).contains(&r) {
let target_sq = f * 9 + r;
sw_beam |= 1u128 << target_sq;
f -= 1;
r += 1;
}
let mut f = file - 1;
let mut r = rank - 1;
while (0..9).contains(&f) && (0..9).contains(&r) {
let target_sq = f * 9 + r;
nw_beam |= 1u128 << target_sq;
f -= 1;
r -= 1;
}
(ne_beam, se_beam, sw_beam, nw_beam)
}
fn generate_rook_beams(sq: i8) -> (u128, u128, u128, u128) {
let file = sq / 9;
let rank = sq % 9;
let mut n_beam = 0u128; let mut e_beam = 0u128; let mut s_beam = 0u128; let mut w_beam = 0u128;
for r in (0..rank).rev() {
let target_sq = file * 9 + r;
n_beam |= 1u128 << target_sq;
}
for f in (file + 1)..9 {
let target_sq = f * 9 + rank;
e_beam |= 1u128 << target_sq;
}
for r in (rank + 1)..9 {
let target_sq = file * 9 + r;
s_beam |= 1u128 << target_sq;
}
for f in (0..file).rev() {
let target_sq = f * 9 + rank;
w_beam |= 1u128 << target_sq;
}
(n_beam, e_beam, s_beam, w_beam)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_generate_square_tables() {
let temp_dir = std::env::temp_dir().join("rsshogi_build_test");
fs::create_dir_all(&temp_dir).unwrap();
generate_square_tables(&temp_dir);
let content = fs::read_to_string(temp_dir.join("square_tables.rs"))
.expect("Failed to read generated file");
assert!(content.contains("SQUARE_TO_FILE"));
assert!(content.contains("SQUARE_TO_RANK"));
assert!(content.contains("[File; 82]")); assert!(content.contains("[Rank; 82]"));
assert!(content.contains("File(0),")); assert!(content.contains("Rank(0),"));
assert!(content.contains("File(-1), // SQ_NONE"));
assert!(content.contains("Rank(-1), // SQ_NONE"));
let _ = fs::remove_dir_all(&temp_dir);
}
}