diff --git a/bot/src/lib.rs b/bot/src/lib.rs index 68ce995..890265f 100644 --- a/bot/src/lib.rs +++ b/bot/src/lib.rs @@ -139,6 +139,14 @@ impl Interface { self.dead = true; } } + + /// Specifies a line that Cold Clear should analyze before making any moves. The path is + /// sensitive to T-spin status. + pub fn force_analysis_line(&mut self, path: Vec) { + if self.send.send(BotMsg::ForceAnalysisLine(path)).is_err() { + self.dead = true; + } + } } enum BotMsg { @@ -148,13 +156,15 @@ enum BotMsg { combo: u32 }, NewPiece(Piece), - NextMove(u32) + NextMove(u32), + ForceAnalysisLine(Vec) } pub struct BotState { tree: TreeState, options: Options, eval: Arc, + forced_analysis_lines: Vec> } pub struct Thinker { @@ -175,7 +185,8 @@ impl BotState { BotState { tree: TreeState::create(board, options.use_hold), options, - eval: Arc::new(eval) + eval: Arc::new(eval), + forced_analysis_lines: vec![], } } @@ -184,7 +195,9 @@ impl BotState { /// Returns `Err(true)` if a thinking cycle can be preformed, but it couldn't find pub fn think(&mut self) -> Result, bool> { if self.tree.nodes < self.options.max_nodes && !self.tree.is_dead() { - if let Some((node, board)) = self.tree.find_and_mark_leaf() { + if let Some((node, board)) = self.tree.find_and_mark_leaf( + &mut self.forced_analysis_lines + ) { return Ok(Thinker { node, board, options: self.options, @@ -216,11 +229,27 @@ impl BotState { } pub fn reset(&mut self, field: [[bool; 10]; 40], b2b: bool, combo: u32) { - self.tree.reset(field, b2b, combo); + let plan = self.tree.get_plan(); + if let Some(garbage_lines) = self.tree.reset(field, b2b, combo) { + for path in &mut self.forced_analysis_lines { + for mv in path { + mv.y += garbage_lines; + } + } + let mut prev_best_path = vec![]; + for mv in plan { + let mut mv = mv.0; + mv.y += garbage_lines; + prev_best_path.push(mv); + } + self.forced_analysis_lines.push(prev_best_path); + } else { + self.forced_analysis_lines.clear(); + } } pub fn min_thinking_reached(&self) -> bool { - self.tree.nodes > self.options.min_nodes + self.tree.nodes > self.options.min_nodes && self.forced_analysis_lines.is_empty() } pub fn next_move(&mut self, incoming: u32, f: impl FnOnce(Move, Info)) -> bool { @@ -260,6 +289,10 @@ impl BotState { true } + + pub fn force_analysis_line(&mut self, path: Vec) { + self.forced_analysis_lines.push(path); + } } impl Thinker { @@ -385,6 +418,7 @@ fn run( board.b2b_bonus = b2b; } Ok(BotMsg::NextMove(incoming)) => do_move = Some(incoming), + Ok(BotMsg::ForceAnalysisLine(path)) => {} } } @@ -404,6 +438,7 @@ fn run( Ok(BotMsg::NewPiece(piece)) => bot.add_next_piece(piece), Ok(BotMsg::Reset { field, b2b, combo }) => bot.reset(field, b2b, combo), Ok(BotMsg::NextMove(incoming)) => do_move = Some(incoming), + Ok(BotMsg::ForceAnalysisLine(path)) => bot.force_analysis_line(path) } if let Some(incoming) = do_move { diff --git a/bot/src/tree.rs b/bot/src/tree.rs index ac629e1..c6020f5 100644 --- a/bot/src/tree.rs +++ b/bot/src/tree.rs @@ -119,7 +119,31 @@ impl, R: Clone> TreeState { this } - pub fn reset(&mut self, field: [[bool; 10]; 40], b2b: bool, combo: u32) { + pub fn reset(&mut self, field: [[bool; 10]; 40], b2b: bool, combo: u32) -> Option { + let garbage_lines; + if b2b == self.board.b2b_bonus && combo == self.board.combo { + let mut b = Board::::new(); + b.set_field(field); + let dif = self.board.column_heights().iter() + .zip(b.column_heights().iter()) + .map(|(&y1, &y2)| y2 - y1) + .min().unwrap(); + let mut is_garbage_receive = true; + for y in 0..(40 - dif) { + if b.get_row(y + dif) != self.board.get_row(y) { + is_garbage_receive = false; + break; + } + } + if is_garbage_receive { + garbage_lines = Some(dif); + } else { + garbage_lines = None; + } + } else { + garbage_lines = None; + } + self.board.set_field(field); self.board.combo = combo; self.board.b2b_bonus = b2b; @@ -146,17 +170,35 @@ impl, R: Clone> TreeState { marked: false, death: false }); + + garbage_lines } /// To be called by a worker looking to expand the tree. `update_known`, `update_speculated`, or /// `unmark` should be called to provide the generated children. If this returns `None`, the /// leaf found is already being expanded by another worker, and you should try again later. - pub fn find_and_mark_leaf(&mut self) -> Option<(NodeId, Board)> { + pub fn find_and_mark_leaf( + &mut self, forced_analysis_lines: &mut Vec> + ) -> Option<(NodeId, Board)> { if self.is_dead() { return None } - let mut current = 0; - loop { + + for i in 0..forced_analysis_lines.len() { + if let Some((node, board, done)) = self.descend(&forced_analysis_lines[i]) { + if done { + forced_analysis_lines.remove(i); + } + return Some((node, board)) + } + } + + self.descend(&[]).map(|(n,b,_)| (n,b)) + } + + fn descend(&mut self, mut path: &[FallingPiece]) -> Option<(NodeId, Board, bool)> { + let mut current = self.root; + 'descend: loop { match self.children[current as usize] { None => { if self.trees[current as usize].marked { @@ -165,12 +207,25 @@ impl, R: Clone> TreeState { self.trees[current as usize].marked = true; return Some(( NodeId(self.generation, current), - self.pieces.rebuild_board(&self.trees[current as usize].board) + self.pieces.rebuild_board(&self.trees[current as usize].board), + path.is_empty() )); } }, - Some(Children::Known(start, len)) => - current = pick(&self.trees, &self.childs[start as usize..(start+len) as usize]), + Some(Children::Known(start, len)) => { + let range = start as usize..(start + len) as usize; + if let [next, rest @ ..] = path { + for c in &self.childs[range.clone()] { + if c.mv == *next { + current = c.node; + path = rest; + continue 'descend; + } + } + } + current = pick(&self.trees, &self.childs[range]); + path = &[]; + } Some(Children::Speculation(c)) => { let mut pick_from = ArrayVec::<[_; 7]>::new(); for (_, c) in c { @@ -182,6 +237,7 @@ impl, R: Clone> TreeState { } let &(start, len) = pick_from.choose(&mut thread_rng()).unwrap(); current = pick(&self.trees, &self.childs[start as usize..(start+len) as usize]); + path = &[]; } } }