Compare commits
5 Commits
d1506e4d6c
...
7c55400fbf
Author | SHA1 | Date | |
---|---|---|---|
7c55400fbf | |||
987ec7656f | |||
30e20d1f66 | |||
bec310c182 | |||
69dfa98bef |
@ -8,6 +8,7 @@ Features:
|
||||
- Piece-square tables
|
||||
- Tapered midgame-endgame evaluation
|
||||
- UCI compatibility
|
||||
- Iterative deepening
|
||||
|
||||
## instructions
|
||||
|
||||
|
@ -12,12 +12,15 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
|
||||
//! Main UCI engine binary.
|
||||
|
||||
use chess_inator::eval::eval_metrics;
|
||||
use chess_inator::fen::FromFen;
|
||||
use chess_inator::movegen::{FromUCIAlgebraic, Move, ToUCIAlgebraic};
|
||||
use chess_inator::search::{best_line, SearchEval};
|
||||
use chess_inator::eval::{eval_metrics};
|
||||
use chess_inator::search::{best_line, InterfaceMsg, SearchEval};
|
||||
use chess_inator::Board;
|
||||
use std::io;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// UCI protocol says to ignore any unknown words.
|
||||
///
|
||||
@ -88,7 +91,18 @@ fn cmd_position(mut tokens: std::str::SplitWhitespace<'_>) -> Board {
|
||||
|
||||
/// Play the game.
|
||||
fn cmd_go(mut _tokens: std::str::SplitWhitespace<'_>, board: &mut Board) {
|
||||
let (line, eval) = best_line(board, None);
|
||||
// interface-to-engine
|
||||
let (tx1, rx) = channel();
|
||||
let tx2 = tx1.clone();
|
||||
|
||||
// timeout
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
let _ = tx2.send(InterfaceMsg::Stop);
|
||||
});
|
||||
|
||||
let (line, eval) = best_line(board, None, Some(rx));
|
||||
|
||||
let chosen = line.last().copied();
|
||||
println!(
|
||||
"info pv{}",
|
||||
|
@ -12,6 +12,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
*/
|
||||
|
||||
use crate::{Board, ColPiece, Color, Square, SquareIdx, BOARD_HEIGHT, BOARD_WIDTH};
|
||||
use crate::hash::Zobrist;
|
||||
|
||||
pub const START_POSITION: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
||||
|
||||
@ -239,6 +240,7 @@ impl FromFen for Board {
|
||||
// parser is always ready to receive another full move digit,
|
||||
// so there is no real "stop" state
|
||||
if matches!(parser_state, FenState::FullMove) {
|
||||
Zobrist::toggle_board_info(&mut pos);
|
||||
Ok(pos)
|
||||
} else {
|
||||
Err(FenError::MissingFields)
|
||||
|
129
src/hash.rs
Normal file
129
src/hash.rs
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
|
||||
This file is part of chess_inator.
|
||||
|
||||
chess_inator is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
chess_inator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with chess_inator. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
*/
|
||||
|
||||
//! Zobrist hash implementation.
|
||||
|
||||
use crate::random::{random_arr_2d_64, random_arr_64};
|
||||
use crate::{
|
||||
Board, CastleRights, ColPiece, Color, Square, BOARD_WIDTH, N_COLORS, N_PIECES, N_SQUARES,
|
||||
};
|
||||
|
||||
const PIECE_KEYS: [[[u64; N_SQUARES]; N_PIECES]; N_COLORS] =
|
||||
[random_arr_2d_64(11), random_arr_2d_64(22)];
|
||||
|
||||
// 4 bits in castle perms -> 16 keys
|
||||
const CASTLE_KEYS: [u64; 16] = random_arr_64(33);
|
||||
|
||||
// ep can be specified by the file
|
||||
const EP_KEYS: [u64; BOARD_WIDTH] = random_arr_64(44);
|
||||
|
||||
// current turn
|
||||
const COL_KEY: [u64; N_COLORS] = random_arr_64(55);
|
||||
|
||||
/// Zobrist hash state.
|
||||
///
|
||||
/// This is not synced to board state, so ensure that all changes made are reflected in the hash
|
||||
/// too.
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug)]
|
||||
pub(crate) struct Zobrist {
|
||||
hash: u64,
|
||||
}
|
||||
|
||||
impl Zobrist {
|
||||
/// Toggle a piece.
|
||||
pub(crate) fn toggle_pc(&mut self, pc: &ColPiece, sq: &Square) {
|
||||
let key = PIECE_KEYS[pc.col as usize][pc.pc as usize][usize::from(sq.0)];
|
||||
self.hash ^= key;
|
||||
}
|
||||
|
||||
/// Toggle an en-passant target square (only square file is used).
|
||||
pub(crate) fn toggle_ep(&mut self, sq: Option<Square>) {
|
||||
if let Some(sq) = sq {
|
||||
let (_r, c) = sq.to_row_col();
|
||||
self.hash ^= EP_KEYS[c];
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle castle rights key.
|
||||
pub(crate) fn toggle_castle(&mut self, castle: &CastleRights) {
|
||||
let bits = ((0x1) & castle.0[0].k as u8)
|
||||
| ((0x2) & castle.0[0].q as u8)
|
||||
| (0x4) & castle.0[1].k as u8
|
||||
| (0x8) & castle.0[1].q as u8;
|
||||
|
||||
self.hash ^= CASTLE_KEYS[bits as usize];
|
||||
}
|
||||
|
||||
/// Toggle player to move.
|
||||
pub(crate) fn toggle_turn(&mut self, turn: Color) {
|
||||
self.hash ^= COL_KEY[turn as usize];
|
||||
}
|
||||
|
||||
/// Toggle all of castling rights, en passant and player to move.
|
||||
///
|
||||
/// This is done because it's simpler to do this every time at the start and end of a
|
||||
/// move/unmove rather than keep track of when castling and ep square and whatever rights
|
||||
/// change. Piece moves, unlike this information, have a centralized implementation.
|
||||
pub(crate) fn toggle_board_info(pos: &mut Board) {
|
||||
pos.zobrist.toggle_ep(pos.ep_square);
|
||||
pos.zobrist.toggle_castle(&pos.castle);
|
||||
pos.zobrist.toggle_turn(pos.turn);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::fen::FromFen;
|
||||
use crate::movegen::{FromUCIAlgebraic, Move};
|
||||
|
||||
/// Zobrist hashes of the same positions should be the same. (basic sanity test)
|
||||
#[test]
|
||||
fn test_zobrist_equality() {
|
||||
let test_cases = [
|
||||
(
|
||||
"4k2r/8/8/8/8/8/8/R3K3 w Qk - 0 1",
|
||||
"4k2r/8/8/8/8/8/8/2KR4 b k - 1 1",
|
||||
"e1c1",
|
||||
),
|
||||
(
|
||||
"4k2r/8/8/8/8/8/8/R3K3 b Qk - 0 1",
|
||||
"5rk1/8/8/8/8/8/8/R3K3 w Q - 1 2",
|
||||
"e8g8",
|
||||
),
|
||||
(
|
||||
"4k3/8/8/8/3p4/8/4P3/4K3 w - - 0 1",
|
||||
"4k3/8/8/8/3pP3/8/8/4K3 b - e3 0 1",
|
||||
"e2e4",
|
||||
),
|
||||
(
|
||||
"4k3/8/8/8/3pP3/8/8/4K3 b - e3 0 1",
|
||||
"4k3/8/8/8/8/4p3/8/4K3 w - - 0 2",
|
||||
"d4e3",
|
||||
),
|
||||
];
|
||||
for (pos1_fen, pos2_fen, mv_uci) in test_cases {
|
||||
eprintln!("tc: {}", pos1_fen);
|
||||
let mut pos1 = Board::from_fen(pos1_fen).unwrap();
|
||||
let hash1_orig = pos1.zobrist;
|
||||
eprintln!("refreshing board 2 '{}'", pos2_fen);
|
||||
let pos2 = Board::from_fen(pos2_fen).unwrap();
|
||||
eprintln!("making mv {}", mv_uci);
|
||||
let mv = Move::from_uci_algebraic(mv_uci).unwrap();
|
||||
let anti_mv = mv.make(&mut pos1);
|
||||
assert_eq!(pos1.zobrist, pos2.zobrist);
|
||||
anti_mv.unmake(&mut pos1);
|
||||
assert_eq!(pos1.zobrist, hash1_orig);
|
||||
}
|
||||
}
|
||||
}
|
10
src/lib.rs
10
src/lib.rs
@ -19,10 +19,13 @@ use std::str::FromStr;
|
||||
|
||||
pub mod eval;
|
||||
pub mod fen;
|
||||
mod hash;
|
||||
pub mod movegen;
|
||||
mod random;
|
||||
pub mod search;
|
||||
|
||||
use crate::fen::{FromFen, ToFen, START_POSITION};
|
||||
use crate::hash::Zobrist;
|
||||
use eval::eval_score::EvalScores;
|
||||
|
||||
const BOARD_WIDTH: usize = 8;
|
||||
@ -457,6 +460,9 @@ pub struct Board {
|
||||
|
||||
/// Counters for evaluation.
|
||||
eval: EvalScores,
|
||||
|
||||
/// Hash state to incrementally update.
|
||||
zobrist: Zobrist,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
@ -477,6 +483,7 @@ impl Board {
|
||||
pl[pc.into()].on_sq(sq);
|
||||
*self.mail.sq_mut(sq) = Some(pc);
|
||||
self.eval.add_piece(&pc, &sq);
|
||||
self.zobrist.toggle_pc(&pc, &sq);
|
||||
dest_pc
|
||||
}
|
||||
|
||||
@ -495,6 +502,7 @@ impl Board {
|
||||
pl[pc.into()].off_sq(sq);
|
||||
*self.mail.sq_mut(sq) = None;
|
||||
self.eval.del_piece(&pc, &sq);
|
||||
self.zobrist.toggle_pc(&pc, &sq);
|
||||
Some(pc)
|
||||
} else {
|
||||
None
|
||||
@ -529,9 +537,11 @@ impl Board {
|
||||
ep_square: self.ep_square.map(|sq| sq.mirror_vert()),
|
||||
castle: CastleRights(self.castle.0),
|
||||
eval: Default::default(),
|
||||
zobrist: Zobrist::default(),
|
||||
};
|
||||
|
||||
new_board.castle.0.reverse();
|
||||
Zobrist::toggle_board_info(&mut new_board);
|
||||
|
||||
for sq in Board::squares() {
|
||||
let opt_pc = self.get_piece(sq.mirror_vert()).map(|pc| ColPiece {
|
||||
|
@ -13,6 +13,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
|
||||
//! Move generation.
|
||||
|
||||
use crate::hash::Zobrist;
|
||||
use crate::fen::ToFen;
|
||||
use crate::{
|
||||
Board, CastleRights, ColPiece, Color, Piece, Square, SquareError, BOARD_HEIGHT, BOARD_WIDTH,
|
||||
@ -97,6 +98,8 @@ pub struct AntiMove {
|
||||
impl AntiMove {
|
||||
/// Undo the move.
|
||||
pub fn unmake(self, pos: &mut Board) {
|
||||
Zobrist::toggle_board_info(pos);
|
||||
|
||||
pos.move_piece(self.dest, self.src);
|
||||
pos.half_moves = self.half_moves;
|
||||
pos.castle = self.castle;
|
||||
@ -146,6 +149,8 @@ impl AntiMove {
|
||||
pos.move_piece(rook_dest, rook_src);
|
||||
}
|
||||
}
|
||||
|
||||
Zobrist::toggle_board_info(pos);
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,6 +184,9 @@ impl Move {
|
||||
ep_square: pos.ep_square,
|
||||
};
|
||||
|
||||
// undo hashes (we will update them at the end of this function)
|
||||
Zobrist::toggle_board_info(pos);
|
||||
|
||||
// reset en passant
|
||||
let ep_square = pos.ep_square;
|
||||
pos.ep_square = None;
|
||||
@ -360,6 +368,9 @@ impl Move {
|
||||
|
||||
pos.turn = pos.turn.flip();
|
||||
|
||||
// redo hashes (we undid them at the start of this function)
|
||||
Zobrist::toggle_board_info(pos);
|
||||
|
||||
anti_move
|
||||
}
|
||||
}
|
||||
|
77
src/random.rs
Normal file
77
src/random.rs
Normal file
@ -0,0 +1,77 @@
|
||||
//! Rust port by dogeystamp <dogeystamp@disroot.org> of
|
||||
//! the pcg64 dxsm random number generator (https://dotat.at/@/2023-06-21-pcg64-dxsm.html)
|
||||
|
||||
struct Pcg64Random {
|
||||
state: u128,
|
||||
inc: u128,
|
||||
}
|
||||
|
||||
/// Generates an array of random numbers.
|
||||
///
|
||||
/// The `rng` parameter only sets the initial state. This function is deterministic and pure.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The array of random numbers, plus the RNG state at the end.
|
||||
const fn pcg64_dxsm<const N: usize>(mut rng: Pcg64Random) -> ([u64; N], Pcg64Random) {
|
||||
let mut ret = [0; N];
|
||||
|
||||
const MUL: u64 = 15750249268501108917;
|
||||
|
||||
let mut i = 0;
|
||||
while i < N {
|
||||
let state: u128 = rng.state;
|
||||
rng.state = state.wrapping_mul(MUL as u128).wrapping_add(rng.inc);
|
||||
let mut hi: u64 = (state >> 64) as u64;
|
||||
let lo: u64 = (state | 1) as u64;
|
||||
hi ^= hi >> 32;
|
||||
hi &= MUL;
|
||||
hi ^= hi >> 48;
|
||||
hi = hi.wrapping_mul(lo);
|
||||
ret[i] = hi;
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
(ret, rng)
|
||||
}
|
||||
|
||||
/// Make an RNG state "sane".
|
||||
const fn pcg64_seed(mut rng: Pcg64Random) -> Pcg64Random {
|
||||
// ensure rng.inc is odd
|
||||
rng.inc = (rng.inc << 1) | 1;
|
||||
rng.state += rng.inc;
|
||||
// one iteration of random
|
||||
let (_, rng) = pcg64_dxsm::<1>(rng);
|
||||
rng
|
||||
}
|
||||
|
||||
/// Generate array of random numbers, based on a seed.
|
||||
///
|
||||
/// This function is pure and deterministic, and also works at compile-time rather than at runtime.
|
||||
///
|
||||
/// Example (generate 10 random numbers):
|
||||
///
|
||||
///```rust
|
||||
/// use crate::random::random_arr_64;
|
||||
/// const ARR: [u64; 10] = random_arr_64(123456);
|
||||
///```
|
||||
pub const fn random_arr_64<const N: usize>(seed: u128) -> [u64; N] {
|
||||
let rng = pcg64_seed(Pcg64Random {
|
||||
// chosen by fair dice roll
|
||||
state: 24437033748623976104561743679864923857,
|
||||
inc: seed,
|
||||
});
|
||||
pcg64_dxsm(rng).0
|
||||
}
|
||||
|
||||
/// Generate 2D array of random numbers based on a seed.
|
||||
pub const fn random_arr_2d_64<const N: usize, const M: usize>(seed: u128) -> [[u64; N]; M] {
|
||||
let mut ret = [[0; N]; M];
|
||||
let mut i = 0;
|
||||
while i < M {
|
||||
ret[i] = random_arr_64(seed);
|
||||
i += 1;
|
||||
}
|
||||
ret
|
||||
}
|
112
src/search.rs
112
src/search.rs
@ -17,6 +17,7 @@ use crate::eval::{Eval, EvalInt};
|
||||
use crate::movegen::{Move, MoveGen, ToUCIAlgebraic};
|
||||
use crate::{Board, Piece};
|
||||
use std::cmp::max;
|
||||
use std::sync::mpsc;
|
||||
|
||||
// min can't be represented as positive
|
||||
const EVAL_WORST: EvalInt = -(EvalInt::MAX);
|
||||
@ -97,16 +98,13 @@ pub struct SearchConfig {
|
||||
alpha_beta_on: bool,
|
||||
/// Limit regular search depth
|
||||
depth: usize,
|
||||
/// Limit quiescence search depth (extra depth on top of regular depth)
|
||||
quiesce_depth: usize,
|
||||
}
|
||||
|
||||
impl Default for SearchConfig {
|
||||
fn default() -> Self {
|
||||
SearchConfig {
|
||||
alpha_beta_on: true,
|
||||
depth: 5,
|
||||
quiesce_depth: 2,
|
||||
depth: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,57 +135,6 @@ fn move_priority(board: &mut Board, mv: &Move) -> EvalInt {
|
||||
eval
|
||||
}
|
||||
|
||||
/// Search past the "horizon" caused by limiting the minmax depth.
|
||||
///
|
||||
/// We'll only search captures.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Absolute (good for current side is positive) evaluation of the position.
|
||||
fn quiesce(
|
||||
board: &mut Board,
|
||||
config: &SearchConfig,
|
||||
depth: usize,
|
||||
mut alpha: EvalInt,
|
||||
beta: EvalInt,
|
||||
) -> EvalInt {
|
||||
if depth == 0 {
|
||||
let eval = board.eval();
|
||||
return eval * EvalInt::from(board.turn.sign());
|
||||
}
|
||||
|
||||
let mut abs_best = None;
|
||||
|
||||
// sort moves by decreasing priority
|
||||
let mut mvs: Vec<_> = board
|
||||
.gen_moves()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.map(|mv| (move_priority(board, &mv), mv))
|
||||
.collect();
|
||||
|
||||
mvs.sort_unstable_by_key(|mv| -mv.0);
|
||||
for (_priority, mv) in mvs {
|
||||
if move_get_capture(board, &mv).is_none() {
|
||||
continue;
|
||||
}
|
||||
let anti_mv = mv.make(board);
|
||||
let abs_score = -quiesce(board, config, depth - 1, -beta, -alpha);
|
||||
anti_mv.unmake(board);
|
||||
if let Some(abs_best_score) = abs_best {
|
||||
abs_best = Some(max(abs_best_score, abs_score));
|
||||
} else {
|
||||
abs_best = Some(abs_score);
|
||||
}
|
||||
alpha = max(alpha, abs_best.unwrap());
|
||||
if alpha >= beta && config.alpha_beta_on {
|
||||
break;
|
||||
}
|
||||
}
|
||||
abs_best.unwrap_or(board.eval() * EvalInt::from(board.turn.sign()))
|
||||
}
|
||||
|
||||
/// Search the game tree to find the absolute (positive good) move and corresponding eval for the
|
||||
/// current player.
|
||||
///
|
||||
@ -214,7 +161,7 @@ fn minmax(
|
||||
let beta = beta.unwrap_or(EVAL_BEST);
|
||||
|
||||
if depth == 0 {
|
||||
let eval = quiesce(board, config, config.quiesce_depth, alpha, beta);
|
||||
let eval = board.eval() * EvalInt::from(board.turn.sign());
|
||||
return (Vec::new(), SearchEval::Centipawns(eval));
|
||||
}
|
||||
|
||||
@ -270,16 +217,56 @@ fn minmax(
|
||||
(best_continuation, abs_best)
|
||||
}
|
||||
|
||||
/// Messages from the interface to the search thread.
|
||||
pub enum InterfaceMsg {
|
||||
Stop,
|
||||
}
|
||||
|
||||
type InterfaceRx = mpsc::Receiver<InterfaceMsg>;
|
||||
|
||||
/// Iteratively deepen search until it is stopped.
|
||||
fn iter_deep(
|
||||
board: &mut Board,
|
||||
config: &SearchConfig,
|
||||
interface: Option<InterfaceRx>,
|
||||
) -> (Vec<Move>, SearchEval) {
|
||||
for depth in 1..=config.depth {
|
||||
let (line, eval) = minmax(board, config, depth, None, None);
|
||||
if let Some(ref rx) = interface {
|
||||
match rx.try_recv() {
|
||||
Ok(msg) => match msg {
|
||||
InterfaceMsg::Stop => return (line, eval),
|
||||
},
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => {}
|
||||
mpsc::TryRecvError::Disconnected => panic!("interface thread stopped"),
|
||||
},
|
||||
}
|
||||
} else if depth == config.depth - 1 {
|
||||
return (line, eval)
|
||||
}
|
||||
}
|
||||
panic!("iterative deepening did not search at all")
|
||||
}
|
||||
|
||||
/// Find the best line (in reverse order) and its evaluation.
|
||||
pub fn best_line(board: &mut Board, config: Option<SearchConfig>) -> (Vec<Move>, SearchEval) {
|
||||
pub fn best_line(
|
||||
board: &mut Board,
|
||||
config: Option<SearchConfig>,
|
||||
interface: Option<InterfaceRx>,
|
||||
) -> (Vec<Move>, SearchEval) {
|
||||
let config = config.unwrap_or_default();
|
||||
let (line, eval) = minmax(board, &config, config.depth, None, None);
|
||||
let (line, eval) = iter_deep(board, &config, interface);
|
||||
(line, eval)
|
||||
}
|
||||
|
||||
/// Find the best move.
|
||||
pub fn best_move(board: &mut Board, config: Option<SearchConfig>) -> Option<Move> {
|
||||
let (line, _eval) = best_line(board, Some(config.unwrap_or_default()));
|
||||
pub fn best_move(
|
||||
board: &mut Board,
|
||||
config: Option<SearchConfig>,
|
||||
interface: Option<InterfaceRx>,
|
||||
) -> Option<Move> {
|
||||
let (line, _eval) = best_line(board, Some(config.unwrap_or_default()), interface);
|
||||
line.last().copied()
|
||||
}
|
||||
|
||||
@ -293,7 +280,6 @@ mod tests {
|
||||
#[test]
|
||||
fn alpha_beta_same_result() {
|
||||
let test_cases = [
|
||||
// in these cases the engines really likes to sacrifice its pieces for no gain...
|
||||
"r2q1rk1/1bp1pp1p/p2p2p1/1p1P2P1/2n1P3/3Q1P2/PbPBN2P/3RKB1R b K - 5 15",
|
||||
"r1b1k2r/p1qpppbp/1p4pn/2B3N1/1PP1P3/2P5/P4PPP/RN1QR1K1 w kq - 0 14",
|
||||
];
|
||||
@ -304,8 +290,8 @@ mod tests {
|
||||
Some(SearchConfig {
|
||||
alpha_beta_on: false,
|
||||
depth: 3,
|
||||
quiesce_depth: Default::default(),
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -316,8 +302,8 @@ mod tests {
|
||||
Some(SearchConfig {
|
||||
alpha_beta_on: true,
|
||||
depth: 3,
|
||||
quiesce_depth: Default::default(),
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user