board_game/games/go/
board.rs

1use std::cmp::Ordering;
2use std::fmt::Debug;
3use std::hash::{Hash, Hasher};
4use std::ops::ControlFlow;
5
6use internal_iterator::InternalIterator;
7use nohash_hasher::IntSet;
8use rand::Rng;
9
10use crate::board::{
11    AllMovesIterator, AvailableMovesIterator, Board, BoardDone, BoardMoves, BoardSymmetry, Outcome, PlayError, Player,
12};
13use crate::games::go::chains::Chains;
14use crate::games::go::tile::Tile;
15use crate::games::go::{PlacementKind, Rules, Territory, TileOccupied, Zobrist, GO_MAX_SIZE};
16use crate::symmetry::D4Symmetry;
17use crate::util::iter::IterExt;
18
19// TODO add must_pass function? maybe even cache the result of that function in board
20#[derive(Clone, Eq, PartialEq)]
21pub struct GoBoard {
22    rules: Rules,
23    chains: Chains,
24    next_player: Player,
25    state: State,
26    history: IntSet<Zobrist>,
27    komi: Komi,
28}
29
30#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
31pub enum Move {
32    Pass,
33    Place(Tile),
34}
35
36#[derive(Debug, Copy, Clone, Eq, PartialEq)]
37pub struct Score {
38    pub a: u32,
39    pub b: u32,
40}
41
42impl Score {
43    /// Komi is the score bonus given to the second player, white for Go but represented as [Player::B] here.
44    pub fn to_outcome(self, komi: Komi) -> Outcome {
45        let score_a = self.a as i32 * 2;
46        let total_b = self.b as i32 * 2 + komi.as_int() as i32;
47        match score_a.cmp(&total_b) {
48            Ordering::Less => Outcome::WonBy(Player::B),
49            Ordering::Equal => Outcome::Draw,
50            Ordering::Greater => Outcome::WonBy(Player::A),
51        }
52    }
53}
54
55/// Komi, in favour of [Player::B] (white).
56#[derive(Debug, Copy, Clone, Eq, PartialEq)]
57pub struct Komi {
58    komi_2: i16,
59}
60
61#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
62pub enum State {
63    Normal,
64    Passed,
65    Done(Outcome),
66}
67
68impl GoBoard {
69    pub fn new(size: u8, komi: Komi, rules: Rules) -> GoBoard {
70        GoBoard {
71            rules,
72            chains: Chains::new(size),
73            next_player: Player::A,
74            state: State::Normal,
75            history: Default::default(),
76            komi,
77        }
78    }
79
80    pub fn from_parts(
81        rules: Rules,
82        chains: Chains,
83        next_player: Player,
84        state: State,
85        history: IntSet<Zobrist>,
86        komi: Komi,
87    ) -> GoBoard {
88        GoBoard {
89            rules,
90            chains,
91            next_player,
92            state,
93            history,
94            komi,
95        }
96    }
97
98    pub fn size(&self) -> u8 {
99        self.chains().size()
100    }
101
102    pub fn area(&self) -> u16 {
103        self.chains().area()
104    }
105
106    pub fn rules(&self) -> Rules {
107        self.rules
108    }
109
110    pub fn komi(&self) -> Komi {
111        self.komi
112    }
113
114    pub fn chains(&self) -> &Chains {
115        &self.chains
116    }
117
118    pub fn state(&self) -> State {
119        self.state
120    }
121
122    pub fn history(&self) -> &IntSet<Zobrist> {
123        &self.history
124    }
125
126    // TODO optimize perft by messing around with history
127    pub fn replace_history(&mut self, history: IntSet<Zobrist>) -> IntSet<Zobrist> {
128        std::mem::replace(&mut self.history, history)
129    }
130
131    pub fn clear_history(&mut self) {
132        self.history.clear();
133    }
134
135    pub fn clone_without_history(&self) -> Self {
136        Self::from_parts(
137            self.rules,
138            self.chains.clone(),
139            self.next_player,
140            self.state,
141            Default::default(),
142            self.komi,
143        )
144    }
145
146    pub fn stone_count(&self) -> u16 {
147        self.chains.stone_count()
148    }
149
150    pub fn stone_count_from(&self, player: Player) -> u16 {
151        self.chains.stone_count_from(player)
152    }
153
154    pub fn empty_count(&self) -> u16 {
155        self.chains.empty_count()
156    }
157
158    pub fn stone_at(&self, tile: Tile) -> Option<Player> {
159        self.chains().stone_at(tile.to_flat(self.size()))
160    }
161
162    pub fn empty_tiles(&self) -> impl ExactSizeIterator<Item = Tile> + '_ {
163        self.chains()
164            .empty_tiles()
165            .pure_map(move |tile| tile.to_tile(self.size()))
166    }
167
168    pub fn current_score(&self) -> Score {
169        self.chains().score()
170    }
171
172    /// The territory map computed using Tromp-Taylor rules.
173    /// The returned vec can be indexed by [FlatTile::index].
174    pub fn territory(&self) -> Vec<Territory> {
175        self.chains().territory()
176    }
177
178    /// Full zobrist, including:
179    /// * the tiles
180    /// * the next player
181    /// * the pass state
182    pub fn zobrist(&self) -> Zobrist {
183        // TODO include rules and komi?
184        let mut result = self.chains().zobrist();
185        result ^= Zobrist::for_color_turn(self.next_player);
186        result ^= Zobrist::for_pass_state(self.state);
187        result
188    }
189
190    pub fn random_available_place_move(&self, rng: &mut impl Rng) -> Result<Option<Move>, BoardDone> {
191        self.check_done()?;
192
193        let tile = self.chains.random_empty_tile_where(rng, |tile| {
194            let mv = Move::Place(tile.to_tile(self.size()));
195            self.is_available_move(mv).unwrap()
196        });
197        let mv = tile.map(|tile| Move::Place(tile.to_tile(self.size())));
198
199        Ok(mv)
200    }
201
202    pub fn assert_valid(&self) {
203        // TODO can we add more asserts?
204        self.chains().assert_valid();
205
206        if !self.rules().needs_history() {
207            assert!(self.history.is_empty())
208        }
209    }
210}
211
212fn is_available_move_sim(rules: &Rules, history: &IntSet<Zobrist>, kind: PlacementKind, next_zobrist: Zobrist) -> bool {
213    // check placement kind
214    match kind {
215        PlacementKind::Normal => {}
216        PlacementKind::Capture => {}
217        PlacementKind::SuicideSingle => return false,
218        PlacementKind::SuicideMulti => {
219            if !rules.allow_multi_stone_suicide {
220                return false;
221            }
222        }
223    }
224
225    // check history
226    //   scan in reverse to hopefully find quicker matches
227    if !rules.allow_repeating_tiles() && history.contains(&next_zobrist) {
228        return false;
229    }
230
231    true
232}
233
234impl Board for GoBoard {
235    type Move = Move;
236
237    fn next_player(&self) -> Player {
238        self.next_player
239    }
240
241    fn is_available_move(&self, mv: Self::Move) -> Result<bool, BoardDone> {
242        self.check_done()?;
243
244        let result = match mv {
245            Move::Pass => true,
246            Move::Place(tile) => {
247                if !tile.exists(self.size()) {
248                    false
249                } else {
250                    let tile = tile.to_flat(self.size());
251                    match self.chains.simulate_place_stone_minimal(tile, self.next_player) {
252                        Ok(sim) => is_available_move_sim(&self.rules, &self.history, sim.kind, sim.next_zobrist),
253                        Err(TileOccupied) => false,
254                    }
255                }
256            }
257        };
258
259        Ok(result)
260    }
261
262    fn play(&mut self, mv: Self::Move) -> Result<(), PlayError> {
263        // usually we'd check if the move is available too, but here we do that later
264        self.check_done()?;
265
266        let curr = self.next_player;
267        let other = curr.other();
268
269        match mv {
270            Move::Pass => {
271                // pass is always available
272                // pass doesn't create history values or care about them
273
274                // auxiliary state update
275                self.next_player = other;
276                self.state = match self.state {
277                    State::Normal => State::Passed,
278                    State::Passed => State::Done(self.current_score().to_outcome(self.komi)),
279                    State::Done(_) => unreachable!(),
280                };
281            }
282            Move::Place(tile) => {
283                let prev_zobrist = self.chains.zobrist();
284
285                // place the tile if the corresponding move is actually available, return error otherwise
286                {
287                    let tile = tile.to_flat(self.size());
288                    let rules = &self.rules;
289                    let history = &self.history;
290                    let place_result = self.chains.place_stone_if(tile, curr, |sim| {
291                        is_available_move_sim(rules, history, sim.kind, sim.next_zobrist)
292                    });
293                    match place_result {
294                        Ok((_, true)) => {}
295                        Ok((_, false)) => return Err(PlayError::UnavailableMove),
296                        Err(TileOccupied) => return Err(PlayError::UnavailableMove),
297                    }
298                }
299
300                // update history
301                if self.rules.needs_history() {
302                    self.history.insert(prev_zobrist);
303                }
304
305                // update auxiliary state
306                self.next_player = other;
307                self.state = State::Normal;
308            }
309        }
310
311        Ok(())
312    }
313
314    fn outcome(&self) -> Option<Outcome> {
315        match self.state {
316            State::Normal | State::Passed => None,
317            State::Done(outcome) => Some(outcome),
318        }
319    }
320
321    fn can_lose_after_move() -> bool {
322        true
323    }
324}
325
326impl<'a> BoardMoves<'a, GoBoard> for GoBoard {
327    type AllMovesIterator = AllMovesIterator<GoBoard>;
328    type AvailableMovesIterator = AvailableMovesIterator<'a, GoBoard>;
329
330    fn all_possible_moves() -> Self::AllMovesIterator {
331        AllMovesIterator::default()
332    }
333
334    fn available_moves(&'a self) -> Result<Self::AvailableMovesIterator, BoardDone> {
335        AvailableMovesIterator::new(self)
336    }
337}
338
339impl Komi {
340    pub fn zero() -> Self {
341        Komi { komi_2: 0 }
342    }
343
344    pub fn new(komi_2: i16) -> Self {
345        Komi { komi_2 }
346    }
347
348    pub fn as_int(self) -> i16 {
349        self.komi_2
350    }
351
352    pub fn as_float(self) -> f32 {
353        self.komi_2 as f32 / 2.0
354    }
355}
356
357impl std::ops::Neg for Komi {
358    type Output = Self;
359
360    fn neg(self) -> Self::Output {
361        Komi { komi_2: -self.komi_2 }
362    }
363}
364
365impl InternalIterator for AllMovesIterator<GoBoard> {
366    type Item = Move;
367
368    fn try_for_each<R, F>(self, mut f: F) -> ControlFlow<R>
369    where
370        F: FnMut(Self::Item) -> ControlFlow<R>,
371    {
372        f(Move::Pass)?;
373        for tile in Tile::all(GO_MAX_SIZE) {
374            f(Move::Place(tile))?;
375        }
376        ControlFlow::Continue(())
377    }
378}
379
380impl InternalIterator for AvailableMovesIterator<'_, GoBoard> {
381    type Item = Move;
382
383    fn try_for_each<R, F>(self, mut f: F) -> ControlFlow<R>
384    where
385        F: FnMut(Self::Item) -> ControlFlow<R>,
386    {
387        // TODO can this be optimized in any way?
388        //   this impl is still better than using `BruteforceMoveIterator`,
389        //     we only check tiles that are actually in the board
390        //   we don't need to keep the move order consistent according to the docs,
391        //     ensure that's right and then switch to using the empty list
392
393        let board = self.board();
394
395        f(Move::Pass)?;
396        for tile in Tile::all(board.size()) {
397            if board.is_available_move(Move::Place(tile)).unwrap() {
398                f(Move::Place(tile))?;
399            }
400        }
401
402        ControlFlow::Continue(())
403    }
404
405    // TODO add optimized count implementation?
406}
407
408impl BoardSymmetry<GoBoard> for GoBoard {
409    type Symmetry = D4Symmetry;
410    type CanonicalKey = Zobrist;
411
412    fn map(&self, sym: D4Symmetry) -> Self {
413        GoBoard {
414            rules: self.rules,
415            chains: self.chains.map_symmetry(sym),
416            next_player: self.next_player,
417            state: self.state,
418            history: self.history.clone(),
419            komi: self.komi,
420        }
421    }
422
423    fn map_move(&self, sym: D4Symmetry, mv: Move) -> Move {
424        match mv {
425            Move::Pass => Move::Pass,
426            Move::Place(tile) => Move::Place(tile.map_symmetry(sym, self.size())),
427        }
428    }
429
430    fn canonical_key(&self) -> Zobrist {
431        // we don't care about the full zobrist here, other fields are not affected by the symmetry
432        self.chains().zobrist()
433    }
434}
435
436impl Hash for GoBoard {
437    // TODO include history (or just len?)
438    fn hash<H: Hasher>(&self, state: &mut H) {
439        self.zobrist().hash(state);
440    }
441}