1use alloc::vec::Vec;
18
19use serde::{Deserialize, Serialize};
20
21use super::Bot;
22use super::Sides;
23use super::determinize::{Determinize, Evaluate};
24use crate::rng::SimRng;
25
26pub trait Order<R: Sides> {
31 fn order(&mut self, rules: &R, commands: Vec<R::Command>) -> Vec<R::Command>;
33}
34
35impl<R: Sides, F: FnMut(&R, Vec<R::Command>) -> Vec<R::Command>> Order<R> for F {
36 fn order(&mut self, rules: &R, commands: Vec<R::Command>) -> Vec<R::Command> {
37 self(rules, commands)
38 }
39}
40
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub struct Unordered;
44
45impl<R: Sides> Order<R> for Unordered {
46 fn order(&mut self, _: &R, commands: Vec<R::Command>) -> Vec<R::Command> {
47 commands
48 }
49}
50
51pub trait Playout<R: Sides> {
58 fn command(&mut self, rules: &R) -> R::Command;
60}
61
62impl<R: Sides, B: Bot<R>> Playout<R> for B {
63 fn command(&mut self, rules: &R) -> R::Command {
64 self.choose(rules)
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70pub enum Horizon {
71 Sides(u32),
74 Turns(u32),
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81#[non_exhaustive]
82pub struct RolloutSettings {
83 pub candidates: u32,
86 pub samples: u32,
88 pub horizon: Horizon,
90 pub commands: u32,
93 pub temperature: u32,
97}
98
99impl RolloutSettings {
100 #[must_use]
104 pub const fn new(
105 candidates: u32,
106 samples: u32,
107 horizon: Horizon,
108 commands: u32,
109 temperature: u32,
110 ) -> Self {
111 Self {
112 candidates,
113 samples,
114 horizon,
115 commands,
116 temperature,
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
127pub struct Rollout<O, P, E> {
128 order: O,
129 playout: P,
130 eval: E,
131 settings: RolloutSettings,
132 rng: SimRng,
133}
134
135impl<O, P, E> Rollout<O, P, E> {
136 pub fn new(order: O, playout: P, eval: E, settings: RolloutSettings, seed: u64) -> Self {
140 Self {
141 order,
142 playout,
143 eval,
144 settings,
145 rng: SimRng::new(seed),
146 }
147 }
148
149 pub fn settings(&self) -> RolloutSettings {
151 self.settings
152 }
153
154 pub fn search<R>(&mut self, rules: &R) -> RolloutSearch<'_, R, O, P, E>
162 where
163 R: Determinize,
164 R::Command: Clone + PartialEq,
165 O: Order<R>,
166 P: Playout<R>,
167 E: Evaluate<R>,
168 {
169 let legal = rules.legal_commands();
170 let mut candidates = self.order.order(rules, legal.clone());
171 candidates.truncate(at_least_one(self.settings.candidates));
172 assert!(
173 !candidates.is_empty(),
174 "no command to choose from: the game lists none, or its order dropped them all"
175 );
176 let seeds = draw_seeds(&mut self.rng, self.settings.samples);
177 let mut playouts: Vec<(usize, u64)> = (0..candidates.len())
178 .flat_map(|candidate| seeds.iter().map(move |&seed| (candidate, seed)))
179 .collect();
180 playouts.reverse();
181 RolloutSearch {
182 root: rules.clone(),
183 side: rules.side_to_move(),
184 legal,
185 sums: alloc::vec![0; candidates.len()],
186 candidates,
187 seeds,
188 playouts,
189 playing: None,
190 chosen: None,
191 bot: self,
192 }
193 }
194}
195
196impl<R, O, P, E> Bot<R> for Rollout<O, P, E>
197where
198 R: Determinize,
199 R::Command: Clone + PartialEq,
200 O: Order<R>,
201 P: Playout<R>,
202 E: Evaluate<R>,
203{
204 fn choose(&mut self, rules: &R) -> R::Command {
211 match self.search(rules).work(u64::MAX) {
212 Progress::Chosen(command) => command,
213 Progress::Working => panic!("a decision took more than u64::MAX units"),
214 }
215 }
216}
217
218pub struct RolloutSearch<'a, R: Sides, O, P, E> {
221 bot: &'a mut Rollout<O, P, E>,
222 root: R,
224 side: R::Side,
225 legal: Vec<R::Command>,
227 candidates: Vec<R::Command>,
228 seeds: Vec<u64>,
229 sums: Vec<i64>,
231 playouts: Vec<(usize, u64)>,
236 playing: Option<Playing<R>>,
237 chosen: Option<R::Command>,
238}
239
240struct Playing<R: Sides> {
242 candidate: usize,
243 copy: R,
244 side: R::Side,
245 changes: u32,
247 played: u32,
249}
250
251impl<R, O, P, E> RolloutSearch<'_, R, O, P, E>
252where
253 R: Determinize,
254 R::Command: Clone + PartialEq,
255 O: Order<R>,
256 P: Playout<R>,
257 E: Evaluate<R>,
258{
259 pub fn advance(&mut self, units: u32) -> Option<R::Command> {
269 match self.work(u64::from(units)) {
270 Progress::Chosen(command) => Some(command),
271 Progress::Working => None,
272 }
273 }
274
275 fn work(&mut self, units: u64) -> Progress<R::Command> {
279 let mut left = units;
280 loop {
281 if let Some(chosen) = &self.chosen {
282 return Progress::Chosen(chosen.clone());
283 }
284 let Some(mut playing) = self.playing.take() else {
285 if left == 0 && !self.playouts.is_empty() {
286 return Progress::Working;
287 }
288 if let Some((candidate, seed)) = self.playouts.pop() {
289 left -= 1;
290 self.playing = Some(self.start(candidate, seed));
291 } else {
292 self.chosen = Some(self.pick());
293 }
294 continue;
295 };
296 if self.over(&playing) {
297 self.finish(&playing);
298 } else if left == 0 {
299 self.playing = Some(playing);
300 return Progress::Working;
301 } else {
302 left -= 1;
303 let command = self.bot.playout.command(&playing.copy);
304 playing.played += 1;
305 if playing.copy.apply(&command).is_ok() {
306 note_side(&mut playing);
307 self.playing = Some(playing);
308 } else {
309 self.finish(&playing);
310 }
311 }
312 }
313 }
314
315 fn start(&self, candidate: usize, seed: u64) -> Playing<R> {
318 let mut copy = checked_copy(&self.root, self.side, &self.legal, seed);
319 apply_candidate(&mut copy, &self.candidates[candidate], candidate, seed);
320 let mut playing = Playing {
321 candidate,
322 copy,
323 side: self.side,
324 changes: 0,
325 played: 0,
326 };
327 note_side(&mut playing);
328 playing
329 }
330
331 fn over(&self, playing: &Playing<R>) -> bool {
334 let settings = self.bot.settings;
335 let reached = match settings.horizon {
336 Horizon::Sides(changes) => playing.changes >= changes,
337 Horizon::Turns(turns) => playing.copy.turn().saturating_sub(self.root.turn()) >= turns,
338 };
339 reached || playing.played >= settings.commands || playing.copy.outcome().is_some()
340 }
341
342 fn finish(&mut self, playing: &Playing<R>) {
344 let score = self.bot.eval.score(&self.root, &playing.copy, self.side);
345 let sum = &mut self.sums[playing.candidate];
346 *sum = sum.saturating_add(i64::from(score));
347 }
348
349 fn pick(&mut self) -> R::Command {
351 let samples = i64::try_from(self.seeds.len()).unwrap_or(i64::MAX);
352 let means: Vec<i64> = self
353 .sums
354 .iter()
355 .map(|sum| sum.div_euclid(samples))
356 .collect();
357 let temperature = self.bot.settings.temperature;
358 let index = pick(&means, temperature, &mut self.bot.rng);
359 self.candidates[index].clone()
360 }
361}
362
363fn note_side<R: Sides>(playing: &mut Playing<R>) {
365 let side = playing.copy.side_to_move();
366 if side != playing.side {
367 playing.side = side;
368 playing.changes = playing.changes.saturating_add(1);
369 }
370}
371
372pub(super) enum Progress<C> {
376 Working,
378 Chosen(C),
380}
381
382pub(super) fn draw_seeds(rng: &mut SimRng, samples: u32) -> Vec<u64> {
385 (0..at_least_one(samples))
386 .map(|_| {
387 let high = u64::from(rng.next_u32());
388 let low = u64::from(rng.next_u32());
389 (high << 32) + low
390 })
391 .collect()
392}
393
394pub(super) fn checked_copy<R: Determinize>(
401 root: &R,
402 side: R::Side,
403 legal: &[R::Command],
404 seed: u64,
405) -> R
406where
407 R::Command: PartialEq,
408{
409 let copy = root.determinize(side, seed);
410 assert!(
411 copy.side_to_move() == side && copy.legal_commands() == legal,
412 "determinize changed the commands at the root (seed {seed}): a copy must list \
413 the state's legal commands, in the same order"
414 );
415 copy
416}
417
418pub(super) fn apply_candidate<R: Determinize>(
425 copy: &mut R,
426 candidate: &R::Command,
427 index: usize,
428 seed: u64,
429) {
430 let applied = copy.apply(candidate).is_ok();
431 assert!(
432 applied,
433 "the copy from determinize refused candidate {index} (seed {seed}): a copy must \
434 accept the state's legal commands"
435 );
436}
437
438pub(super) fn at_least_one(n: u32) -> usize {
440 usize::try_from(n.max(1)).unwrap_or(usize::MAX)
441}
442
443pub(super) fn pick(means: &[i64], temperature: u32, rng: &mut SimRng) -> usize {
447 let best = means.iter().copied().max().unwrap_or(0);
448 let best_index = means.iter().position(|&mean| mean == best).unwrap_or(0);
449 if temperature == 0 {
450 return best_index;
451 }
452 let weights: Vec<u64> = means
453 .iter()
454 .map(|&mean| {
455 let below = (best - mean).saturating_mul(1 << 16) / i64::from(temperature);
456 u64::from(exp2_q16(-below))
457 })
458 .collect();
459 let shift = fit_shift(weights.iter().sum());
460 let weights: Vec<u64> = weights.iter().map(|weight| weight >> shift).collect();
461 let total = u32::try_from(weights.iter().sum::<u64>()).unwrap_or(u32::MAX);
462 share(&weights, u64::from(rng.roll_below(total))).unwrap_or(best_index)
463}
464
465fn fit_shift(total: u64) -> u32 {
467 (total >> 32).bit_width()
468}
469
470fn share(weights: &[u64], mut roll: u64) -> Option<usize> {
473 for (index, &weight) in weights.iter().enumerate() {
474 if roll < weight {
475 return Some(index);
476 }
477 roll -= weight;
478 }
479 None
480}
481
482const EXP2_FRACTIONS: [u32; 16] = [
484 65536, 62757, 60097, 57549, 55109, 52773, 50535, 48393, 46341, 44376, 42495, 40693, 38968,
485 37316, 35734, 34219,
486];
487
488pub fn exp2_q16(x: i64) -> u32 {
493 let down = x.min(0).unsigned_abs();
494 let halvings = u32::try_from(down >> 16).unwrap_or(u32::MAX);
495 let fraction = down & 0xFFFF;
496 let entry = usize::try_from(fraction >> 12).unwrap_or(0);
497 let high = u64::from(EXP2_FRACTIONS[entry]);
498 let low = EXP2_FRACTIONS
499 .get(entry + 1)
500 .map_or(u64::from(EXP2_FRACTIONS[0]) >> 1, |&next| u64::from(next));
501 let value = high - (((high - low) * (fraction & 0xFFF)) >> 12);
502 let value = value.checked_shr(halvings).unwrap_or(0);
503 u32::try_from(value).unwrap_or(0)
504}
505
506#[cfg(test)]
507mod tests;