board_game/games/go/
chains.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2use std::fmt::Debug;
3use std::hash::{Hash, Hasher};
4
5use itertools::Itertools;
6use rand::seq::IteratorRandom;
7use rand::Rng;
8
9use crate::board::Player;
10use crate::games::go::link::{LinkHead, LinkNode};
11use crate::games::go::stack_vec::StackVec4;
12use crate::games::go::tile::Tile;
13use crate::games::go::util::OptionU16;
14use crate::games::go::{FlatTile, Linked, Score, Zobrist, GO_MAX_SIZE};
15use crate::symmetry::{D4Symmetry, Symmetry};
16use crate::util::iter::IterExt;
17
18// TODO add function to remove stones?
19//   could be tricky since groups would have to be split
20//   could be pretty slow, we never need fast stone removal (except maybe for perft?)
21
22// TODO do a bunch of struct-of-array instead of array-of-struct stuff?
23//   unfortunately that would require allocating more separate vecs
24
25#[derive(Clone)]
26pub struct Chains {
27    size: u8,
28
29    // TODO combine these into one vec to reduce allocations?
30    tiles: Vec<TileContent>,
31    groups: Vec<Group>,
32
33    // derived data
34    stones_a: u16,
35    empty_list: LinkHead,
36    dead_groups: LinkHead,
37    zobrist: Zobrist,
38}
39
40// TODO compact into single u8
41// TODO store the current tile in the content too without the extra indirection?
42#[derive(Debug, Clone)]
43pub struct TileContent {
44    pub group_id: OptionU16,
45    pub link: LinkNode,
46}
47
48// TODO compact? we can at least force player into one of the other fields
49// TODO do even even need player here if we also store the player in the tile itself?
50#[derive(Debug, Clone)]
51pub struct Group {
52    pub color: Player,
53    /// The number of edges adjacent to to liberties.
54    /// This forms an upper bound on the true number of liberties.
55    /// This is easier to compute incrementally but still enough to know if a group is dead.
56    pub liberty_edge_count: u16,
57    // TODO also track the real liberty count?
58    //   not necessary for correctness, just for heuristics and convenience
59    /// The combined hash of all stones in this group.
60    /// Used to quickly remove the entire group from the hash.
61    pub zobrist: Zobrist,
62
63    /// The stones that are part of this group.
64    pub stones: LinkHead,
65    /// Link in the dead group list.
66    pub dead_link: LinkNode,
67}
68
69#[derive(Debug, Clone, Eq, PartialEq)]
70pub struct MinimalPlacement {
71    pub kind: PlacementKind,
72    pub next_zobrist: Zobrist,
73}
74
75// TODO replace pub with getters to ensure they don't get tampered with?
76// TODO remove as much computation from this as possible again, it's slowing things down
77//    we really only need the zobrist, leave other state for some separate simulate function
78// TODO remove tile, color, counts?
79#[derive(Debug, Clone, Eq, PartialEq)]
80pub struct SimulatedPlacement {
81    pub tile: FlatTile,
82    pub color: Player,
83    pub kind: PlacementKind,
84
85    // stats right after the stone is placed, before removing dead groups
86    pub new_group_stone_count: u16,
87    pub new_group_initial_liberty_edge_count: u16,
88
89    // the groups that will be merged/removed
90    pub merge_friendly: StackVec4,
91    pub clear_enemy: StackVec4,
92
93    // the state of the board after this move
94    pub next_zobrist: Zobrist,
95    pub next_stone_count: u16,
96}
97
98#[derive(Debug, Copy, Clone, Eq, PartialEq)]
99pub enum PlacementKind {
100    Normal,
101    Capture,
102    // TODO merge with count field?
103    SuicideSingle,
104    SuicideMulti,
105}
106
107#[derive(Debug, Copy, Clone, Eq, PartialEq)]
108pub struct TileOccupied;
109
110#[derive(Debug, Copy, Clone, Eq, PartialEq)]
111pub enum Territory {
112    Stone(Player),
113    Territory(Player),
114    Both,
115    Neither,
116}
117
118impl Chains {
119    pub fn new(size: u8) -> Self {
120        assert!(size <= GO_MAX_SIZE);
121
122        let area = size as u16 * size as u16;
123        let tiles = (0..area)
124            .map(|i| TileContent {
125                group_id: OptionU16::None,
126                link: LinkNode::full(area, i),
127            })
128            .collect_vec();
129
130        Chains {
131            size,
132            tiles,
133            groups: vec![],
134            stones_a: 0,
135            empty_list: LinkHead::full(area),
136            zobrist: Zobrist::default(),
137            dead_groups: LinkHead::empty(),
138        }
139    }
140
141    pub fn size(&self) -> u8 {
142        self.size
143    }
144
145    pub fn area(&self) -> u16 {
146        self.size as u16 * self.size as u16
147    }
148
149    pub fn stone_count(&self) -> u16 {
150        self.area() - self.empty_count()
151    }
152
153    pub fn stone_count_from(&self, player: Player) -> u16 {
154        match player {
155            Player::A => self.stones_a,
156            Player::B => self.stone_count() - self.stones_a,
157        }
158    }
159
160    pub fn empty_count(&self) -> u16 {
161        self.empty_list.len()
162    }
163
164    // TODO find a batter way to expose internal state
165    //   eg. do we want to expose the group id?
166    //   how can a user iterate over the stones in a group?
167    pub fn content_at(&self, tile: FlatTile) -> &TileContent {
168        &self.tiles[tile.index() as usize]
169    }
170
171    pub fn group_at(&self, tile: FlatTile) -> Option<&Group> {
172        self.content_at(tile)
173            .group_id
174            .to_option()
175            .map(|id| &self.groups[id as usize])
176    }
177
178    pub fn stone_at(&self, tile: FlatTile) -> Option<Player> {
179        self.group_at(tile).map(|group| group.color)
180    }
181
182    pub fn zobrist(&self) -> Zobrist {
183        self.zobrist
184    }
185
186    /// Iterator over all of the groups that currently exist.
187    /// The items are `(group_id, group)`. `group_id` is not necessarily continuous.
188    pub fn groups(&self) -> impl Iterator<Item = (u16, &Group)> + '_ {
189        // TODO implement exact size iterator for this? we can cache the number of alive groups
190        self.groups
191            .iter()
192            .enumerate()
193            .filter(|(_, group)| !group.is_dead())
194            .pure_map(|(id, group)| (id as u16, group))
195    }
196
197    pub fn tiles(&self) -> &[TileContent] {
198        &self.tiles
199    }
200
201    pub fn empty_tiles(&self) -> impl ExactSizeIterator<Item = FlatTile> + '_ {
202        self.empty_list.iter(&self.tiles).pure_map(FlatTile::new)
203    }
204
205    pub fn random_empty_tile(&self, rng: &mut impl Rng) -> Option<FlatTile> {
206        self.random_empty_tile_where(rng, |_| true)
207    }
208
209    /// Uniformly sample an empty tile for which `f(tile)` is true.
210    ///
211    /// This implementation is optimized assuming `f` is very likely to return true.
212    pub fn random_empty_tile_where(&self, rng: &mut impl Rng, mut f: impl FnMut(FlatTile) -> bool) -> Option<FlatTile> {
213        if self.empty_list.is_empty() {
214            return None;
215        }
216
217        // TODO optimize sampling coefficients, use a mix of different sizes to profile
218        const FULL_SAMPLE_MIN_EMPTY: u16 = 8;
219        const FULL_SAMPLE_MIN_EMPTY_FRAC: f32 = 0.2;
220        const FULL_SAMPLE_TRIES: u32 = 16;
221        const EMPTY_SAMPLE_TRIES: u32 = 16;
222
223        const FRAC_DENOM: u32 = 128;
224        const FRAC_NUMER: u32 = (FULL_SAMPLE_MIN_EMPTY_FRAC * FRAC_DENOM as f32) as u32;
225
226        // if there are enough empty times, just randomly sample until we find one
227        let empty_count = self.empty_count();
228        let empty_per_64 = empty_count as u32 * FRAC_DENOM / self.area() as u32;
229        if empty_count >= FULL_SAMPLE_MIN_EMPTY && empty_per_64 > FRAC_NUMER {
230            for _ in 0..EMPTY_SAMPLE_TRIES {
231                let tile = FlatTile::new(rng.gen_range(0..self.area()));
232                if self.stone_at(tile).is_none() && f(tile) {
233                    return Some(tile);
234                }
235            }
236        }
237
238        // partial fallback: sample random empty tiles and check if they match
239        for _ in 0..FULL_SAMPLE_TRIES {
240            let tile = self.empty_tiles().choose(rng).unwrap();
241            if f(tile) {
242                return Some(tile);
243            }
244        }
245
246        // full fallback: sample from fully filtered list
247        //   this ensures that we only return None if we have actually checked all empty tiles
248        self.empty_tiles().filter(|&tile| f(tile)).choose(rng)
249    }
250
251    pub fn territory(&self) -> Vec<Territory> {
252        self.compute_territory(true)
253    }
254
255    pub fn score(&self) -> Score {
256        let territory = self.compute_territory(false);
257
258        let mut score_a = self.stone_count_from(Player::A) as u32;
259        let mut score_b = self.stone_count_from(Player::B) as u32;
260
261        for tile in self.empty_tiles() {
262            match territory[tile.index() as usize] {
263                Territory::Stone(_) => unreachable!(),
264                Territory::Territory(Player::A) => score_a += 1,
265                Territory::Territory(Player::B) => score_b += 1,
266                Territory::Both | Territory::Neither => {}
267            }
268        }
269
270        Score { a: score_a, b: score_b }
271    }
272
273    fn compute_territory(&self, include_stones: bool) -> Vec<Territory> {
274        let size = self.size;
275        let area = self.area();
276
277        // empty tiles whose info should be propagated to neighbors
278        let mut todo = VecDeque::new();
279
280        // initialize the state
281        let mut state = vec![Territory::Neither; area as usize];
282        if include_stones {
283            for tile in FlatTile::all(size) {
284                if let Some(player) = self.stone_at(tile) {
285                    state[tile.index() as usize] = Territory::Stone(player);
286                }
287            }
288        }
289
290        // collect starting points
291        for tile in self.empty_tiles() {
292            let mut reach_a = false;
293            let mut reach_b = false;
294            tile.all_adjacent(size).for_each(|adj| {
295                let stone = self.stone_at(adj);
296                reach_a |= stone == Some(Player::A);
297                reach_b |= stone == Some(Player::B);
298            });
299            if reach_a || reach_b {
300                state[tile.index() as usize] = Territory::from_reach(reach_a, reach_b);
301                todo.push_back(tile)
302            }
303        }
304
305        // propagate
306        while let Some(curr) = todo.pop_front() {
307            let curr_state = state[curr.index() as usize];
308
309            for adj in curr.all_adjacent(size) {
310                if self.stone_at(adj).is_none() {
311                    let old_state = state[adj.index() as usize];
312                    let new_state = old_state.merge_empty(curr_state);
313                    if new_state != old_state {
314                        state[adj.index() as usize] = new_state;
315                        todo.push_back(adj);
316                    }
317                }
318            }
319        }
320
321        state
322    }
323
324    pub fn place_stone(&mut self, tile: FlatTile, color: Player) -> Result<SimulatedPlacement, TileOccupied> {
325        self.place_stone_if(tile, color, |_| true).map(|(sim, _)| sim)
326    }
327
328    pub fn place_stone_if(
329        &mut self,
330        tile: FlatTile,
331        color: Player,
332        mut f: impl FnMut(&SimulatedPlacement) -> bool,
333    ) -> Result<(SimulatedPlacement, bool), TileOccupied> {
334        let simulated = self.simulate_place_stone(tile, color)?;
335
336        let cond = f(&simulated);
337        if cond {
338            self.apply_simulated_placement(&simulated);
339        }
340
341        Ok((simulated, cond))
342    }
343
344    pub fn simulate_place_stone_minimal(
345        &self,
346        tile: FlatTile,
347        color: Player,
348    ) -> Result<MinimalPlacement, TileOccupied> {
349        let size = self.size;
350
351        let content = &self.tiles[tile.index() as usize];
352        if content.group_id.is_some() {
353            return Err(TileOccupied);
354        }
355
356        let mut merged_group_initial_liberty_edge_count = 0;
357        let mut any_merged = false;
358        let mut any_captured = false;
359
360        let mut zobrist_with_captures = self.zobrist;
361        let mut zobrist_merged = Zobrist::default();
362
363        let mut adjacent_groups = StackVec4::new();
364        for (adj_i, adj) in tile.all_adjacent(size).enumerate() {
365            let content = self.content_at(adj);
366
367            match content.group_id.to_option() {
368                None => merged_group_initial_liberty_edge_count += 1,
369                Some(group_id) => {
370                    let group = &self.groups[group_id as usize];
371
372                    adjacent_groups[adj_i] = group_id;
373                    let group_adjacency_count = adjacent_groups.count(group_id);
374
375                    if group.color == color {
376                        any_merged = true;
377                        if group_adjacency_count == 1 {
378                            zobrist_merged ^= group.zobrist;
379                            merged_group_initial_liberty_edge_count += group.liberty_edge_count;
380                        }
381                        merged_group_initial_liberty_edge_count -= 1;
382                    } else {
383                        debug_assert!(group.liberty_edge_count as usize >= group_adjacency_count);
384                        if group.liberty_edge_count as usize == group_adjacency_count {
385                            zobrist_with_captures ^= group.zobrist;
386                            any_captured = true;
387                        }
388                    }
389                }
390            }
391        }
392
393        // determine what kind of placement this is
394        let kind = if any_captured {
395            PlacementKind::Capture
396        } else if merged_group_initial_liberty_edge_count == 0 {
397            if any_merged {
398                PlacementKind::SuicideMulti
399            } else {
400                PlacementKind::SuicideSingle
401            }
402        } else {
403            PlacementKind::Normal
404        };
405
406        let next_zobrist = if kind.is_suicide() {
407            // remove multi-suicide stones if any
408            zobrist_with_captures ^ zobrist_merged
409        } else {
410            // add newly placed stone
411            let zobrist_stone = Zobrist::for_color_tile(color, tile);
412            zobrist_with_captures ^ zobrist_stone
413        };
414
415        Ok(MinimalPlacement { kind, next_zobrist })
416    }
417
418    // TODO unroll this whole thing into the 4 directions?
419    //    collected inputs: type/group_id/liberties for each size
420    //    outputs: what groups to merge and what groups to kill
421    //    => simple function with 4 inputs, 4 outputs
422    #[allow(clippy::collapsible_else_if)]
423    pub fn simulate_place_stone(&self, tile: FlatTile, color: Player) -> Result<SimulatedPlacement, TileOccupied> {
424        let size = self.size;
425        let content = &self.tiles[tile.index() as usize];
426
427        if content.group_id.is_some() {
428            return Err(TileOccupied);
429        }
430
431        // investigate adjacent tiles
432        let mut new_group_initial_liberty_edge_count = 0;
433        let mut merged_zobrist = Zobrist::default();
434        let mut merged_count = 0;
435        let mut captured_zobrist = Zobrist::default();
436        let mut captured_stone_count = 0;
437
438        // TODO get rid of adjacent_groups, it's just redundant with enemy and friendly
439        let mut adjacent_groups = StackVec4::new();
440        let mut clear_enemy = StackVec4::new();
441        let mut merge_friendly = StackVec4::new();
442
443        // TODO unroll?
444        for (adj_i, adj) in tile.all_adjacent(size).enumerate() {
445            let content = self.content_at(adj);
446
447            match content.group_id.to_option() {
448                None => new_group_initial_liberty_edge_count += 1,
449                Some(group_id) => {
450                    let group = &self.groups[group_id as usize];
451
452                    adjacent_groups[adj_i] = group_id;
453                    let group_adjacency_count = adjacent_groups.count(group_id);
454
455                    if group.color == color {
456                        if group_adjacency_count == 1 {
457                            merged_count += group.stones.len();
458                            new_group_initial_liberty_edge_count += group.liberty_edge_count;
459                            merged_zobrist ^= group.zobrist;
460                            merge_friendly[adj_i] = group_id;
461                        }
462                        new_group_initial_liberty_edge_count -= 1;
463                    } else {
464                        debug_assert!(group.liberty_edge_count as usize >= group_adjacency_count);
465                        if group.liberty_edge_count as usize == group_adjacency_count {
466                            clear_enemy[adj_i] = group_id;
467                            captured_stone_count += group.stones.len();
468                            captured_zobrist ^= group.zobrist;
469                        }
470                    }
471                }
472            }
473        }
474
475        debug_assert!(!merge_friendly.contains_duplicates());
476        debug_assert!(!clear_enemy.contains_duplicates());
477
478        // decide what kind of placement this is
479        let kind = if !clear_enemy.is_empty() {
480            PlacementKind::Capture
481        } else if new_group_initial_liberty_edge_count == 0 {
482            if merged_count == 0 {
483                PlacementKind::SuicideSingle
484            } else {
485                PlacementKind::SuicideMulti
486            }
487        } else {
488            PlacementKind::Normal
489        };
490
491        // calculate final stats
492        let mut next_zobrist = self.zobrist ^ captured_zobrist;
493        let mut next_stone_count = self.stone_count() - captured_stone_count;
494        match kind {
495            PlacementKind::Normal | PlacementKind::Capture => {
496                next_zobrist ^= Zobrist::for_color_tile(color, tile);
497                next_stone_count += 1;
498            }
499            PlacementKind::SuicideSingle => {}
500            PlacementKind::SuicideMulti => {
501                next_zobrist ^= merged_zobrist;
502                next_stone_count -= merged_count;
503            }
504        }
505
506        // TODO include new_group_zobrist?
507        Ok(SimulatedPlacement {
508            tile,
509            color,
510            kind,
511            new_group_stone_count: 1 + merged_count,
512            new_group_initial_liberty_edge_count,
513            merge_friendly,
514            clear_enemy,
515            next_zobrist,
516            next_stone_count,
517        })
518    }
519
520    pub fn apply_simulated_placement(&mut self, simulated: &SimulatedPlacement) {
521        let &SimulatedPlacement {
522            tile,
523            color,
524            kind,
525            new_group_stone_count,
526            new_group_initial_liberty_edge_count,
527            ref merge_friendly,
528            ref clear_enemy,
529            next_zobrist,
530            next_stone_count,
531        } = simulated;
532
533        let size = self.size();
534        let tile_index = tile.index();
535
536        match kind {
537            PlacementKind::Normal | PlacementKind::Capture => {
538                // update new tile
539                //   the group id will be set later when updating all stones in the friendly group
540                let tile_zobrist = Zobrist::for_color_tile(color, tile);
541                self.zobrist ^= tile_zobrist;
542                self.empty_list.remove(tile_index, &mut self.tiles);
543                if color == Player::A {
544                    self.stones_a += 1;
545                }
546
547                // fast case: no clearing, no real merging
548                if merge_friendly.len() <= 1 && clear_enemy.is_empty() {
549                    let group_id = if let Some(group_id) = merge_friendly.first() {
550                        // merge into single adjacent friendly group
551                        let group = &mut self.groups[group_id as usize];
552
553                        group.zobrist ^= tile_zobrist;
554                        group.stones.insert_front(tile_index, &mut self.tiles);
555                        group.liberty_edge_count = new_group_initial_liberty_edge_count;
556
557                        group_id
558                    } else {
559                        // no adjacent, allocate new group
560                        self.allocate_group(Group {
561                            color,
562                            stones: LinkHead::single(tile_index),
563                            liberty_edge_count: new_group_initial_liberty_edge_count,
564                            zobrist: tile_zobrist,
565                            dead_link: LinkNode::single(),
566                        })
567                    };
568
569                    // set tile itself
570                    self.tiles[tile_index as usize].group_id = OptionU16::Some(group_id);
571
572                    // decrement adjacent liberties
573                    change_liberty_edges_at(
574                        size,
575                        &mut self.tiles,
576                        &mut self.groups,
577                        tile,
578                        -1,
579                        OptionU16::Some(group_id),
580                    );
581                } else {
582                    // build new merged group
583                    //   do this before modifying liberties so they're immediately counted for the new group
584                    let new_group_id =
585                        self.build_merged_group(tile, color, merge_friendly, new_group_initial_liberty_edge_count);
586                    debug_assert_eq!(new_group_stone_count, self.groups[new_group_id as usize].stones.len());
587
588                    // remove liberties from stones adjacent to tile
589                    change_liberty_edges_at(
590                        size,
591                        &mut self.tiles,
592                        &mut self.groups,
593                        tile,
594                        -1,
595                        OptionU16::Some(new_group_id),
596                    );
597
598                    // remove cleared groups
599                    clear_enemy.for_each(|clear_group_id| {
600                        self.clear_group(clear_group_id);
601                    });
602                }
603            }
604            PlacementKind::SuicideSingle => {
605                // don't do anything, we don't even need to place the stone
606            }
607            PlacementKind::SuicideMulti => {
608                merge_friendly.for_each(|clear_group_id| {
609                    self.clear_group(clear_group_id);
610                });
611            }
612        }
613
614        debug_assert_eq!(self.zobrist, next_zobrist);
615        debug_assert_eq!(self.stone_count(), next_stone_count);
616    }
617
618    // TODO merge into largest existing merged group so we can skip changing those tiles?
619    fn build_merged_group(
620        &mut self,
621        tile: FlatTile,
622        color: Player,
623        merge_friendly: &StackVec4,
624        new_group_initial_liberty_edge_count: u16,
625    ) -> u16 {
626        let tile_index = tile.index();
627
628        // track stats for new group
629        let mut new_group_stones = LinkHead::single(tile_index);
630        let mut new_group_zobrist = Zobrist::for_color_tile(color, tile);
631
632        // merge in other groups
633        merge_friendly.for_each(|merge_group_id| {
634            let merge_group = &mut self.groups[merge_group_id as usize];
635
636            new_group_stones.splice_front_take(&mut merge_group.stones, &mut self.tiles);
637            new_group_zobrist ^= merge_group.zobrist;
638
639            self.free_group(merge_group_id);
640        });
641
642        // allocate new group
643        let new_group_id = self.allocate_group(Group {
644            color,
645            stones: new_group_stones.clone(),
646            liberty_edge_count: new_group_initial_liberty_edge_count,
647            zobrist: new_group_zobrist,
648            dead_link: LinkNode::single(),
649        });
650
651        // mark tiles as part of new group
652        new_group_stones.for_each_mut(&mut self.tiles, |tiles, tile_index| {
653            tiles[tile_index as usize].group_id = OptionU16::Some(new_group_id);
654        });
655
656        new_group_id
657    }
658
659    fn clear_group(&mut self, group_id: u16) {
660        let size = self.size();
661        let clear_group = &mut self.groups[group_id as usize];
662
663        // remove group from global state
664        self.zobrist ^= clear_group.zobrist;
665        if clear_group.color == Player::A {
666            self.stones_a -= clear_group.stones.len();
667        }
668
669        // fix per-tile state
670        //  unfortunately we have to do some borrowing trickery
671        {
672            let stones = clear_group.stones.clone();
673            let tiles = &mut self.tiles;
674            let groups = &mut self.groups;
675
676            stones.for_each_mut(tiles, |tiles, tile_index| {
677                // map params
678                let tile = FlatTile::new(tile_index);
679
680                // remove stone->group link
681                tiles[tile_index as usize].group_id = OptionU16::None;
682
683                // increase liberties of surrounding groups
684                //    we might accidentally increment old group liberties here, but that shouldn't be a problem
685                change_liberty_edges_at(size, tiles, groups, tile, 1, OptionU16::None);
686            });
687        }
688
689        let clear_group = &mut self.groups[group_id as usize];
690
691        // add all stones to empty list
692        self.empty_list
693            .splice_front_take(&mut clear_group.stones, &mut self.tiles);
694
695        // mark group as dead
696        self.free_group(group_id);
697    }
698
699    fn allocate_group(&mut self, new: Group) -> u16 {
700        match self.dead_groups.pop_front(&mut self.groups).to_option() {
701            Some(id) => {
702                self.groups[id as usize] = new;
703                id
704            }
705            None => {
706                let id = self.groups.len() as u16;
707                self.groups.push(new);
708                id
709            }
710        }
711    }
712
713    fn free_group(&mut self, id: u16) {
714        let group = &mut self.groups[id as usize];
715        debug_assert!(group.stones.is_empty());
716        debug_assert!(group.dead_link.is_unconnected_or_single());
717
718        // mark group itself as dead
719        self.groups[id as usize] = Group {
720            color: group.color,
721            stones: LinkHead::empty(),
722            liberty_edge_count: 0,
723            zobrist: Default::default(),
724            dead_link: LinkNode::single(),
725        };
726
727        // insert into empty list
728        self.dead_groups.insert_front(id, &mut self.groups);
729    }
730
731    pub fn map_symmetry(&self, sym: D4Symmetry) -> Chains {
732        let size = self.size();
733        let map_tile_index = |index: u16| {
734            FlatTile::new(index)
735                .to_tile(size)
736                .map_symmetry(sym, size)
737                .to_flat(size)
738                .index()
739        };
740
741        let new_tiles = Tile::all(size)
742            .map(|new_tile| {
743                let old_tile = new_tile.map_symmetry(sym.inverse(), size);
744                let old_flat = old_tile.to_flat(size);
745                let old_content = self.tiles[old_flat.index() as usize].clone();
746
747                TileContent {
748                    group_id: old_content.group_id,
749                    link: old_content.link.map_index(map_tile_index),
750                }
751            })
752            .collect_vec();
753
754        let mut new_zobrist = Zobrist::default();
755
756        let new_groups = self
757            .groups
758            .iter()
759            .map(|old_group| {
760                let new_stones = old_group.stones.map_index(map_tile_index);
761
762                let new_group_zobrist = new_stones
763                    .iter(&new_tiles)
764                    .map(|tile| Zobrist::for_color_tile(old_group.color, FlatTile::new(tile)))
765                    .fold(Zobrist::default(), |a, b| a ^ b);
766
767                new_zobrist ^= new_group_zobrist;
768
769                Group {
770                    color: old_group.color,
771                    liberty_edge_count: old_group.liberty_edge_count,
772                    zobrist: new_group_zobrist,
773                    stones: new_stones,
774                    dead_link: old_group.dead_link.clone(),
775                }
776            })
777            .collect_vec();
778
779        Chains {
780            size: self.size,
781            tiles: new_tiles,
782            groups: new_groups,
783            stones_a: self.stones_a,
784            empty_list: self.empty_list.map_index(map_tile_index),
785            dead_groups: self.dead_groups.clone(),
786            zobrist: new_zobrist,
787        }
788    }
789
790    pub fn assert_valid(&self) {
791        let size = self.size();
792
793        // check per-tile stuff and collect info
794        let mut group_info = HashMap::new();
795        let mut empty_tiles = HashSet::new();
796        let mut stone_count = 0;
797        let mut stone_count_a = 0;
798        let mut stone_count_b = 0;
799
800        for tile in FlatTile::all(size) {
801            let content = self.content_at(tile);
802
803            if let Some(id) = content.group_id.to_option() {
804                // group must must exist
805                assert!((id as usize) < self.groups.len());
806                let group = &self.groups[id as usize];
807
808                // group must be alive
809                assert!(!group.is_dead());
810
811                // track info
812                let group_zobrist = group_info.entry(id).or_insert((Zobrist::default(), HashSet::default()));
813                group_zobrist.0 ^= Zobrist::for_color_tile(group.color, tile);
814                group_zobrist.1.insert(tile.index());
815
816                stone_count += 1;
817                match group.color {
818                    Player::A => stone_count_a += 1,
819                    Player::B => stone_count_b += 1,
820                }
821            } else {
822                empty_tiles.insert(tile.index());
823            }
824        }
825
826        assert_eq!(self.stone_count(), stone_count);
827        assert_eq!(self.stone_count_from(Player::A), stone_count_a);
828        assert_eq!(self.stone_count_from(Player::B), stone_count_b);
829
830        let mut expected_dead_groups = HashSet::new();
831
832        // check per-group stuff
833        for (id, group) in self.groups.iter().enumerate() {
834            // stone_count and liberty_edge_count must agree on whether the group is dead
835            assert_eq!(group.stones.is_empty(), (group.liberty_edge_count == 0));
836
837            // groups must be used xor dead
838            let is_dead = group.stones.is_empty();
839            assert_ne!(group_info.contains_key(&(id as u16)), is_dead);
840
841            let linked_stones = group.stones.assert_valid_and_collect(&self.tiles);
842
843            // group zobrist must be correct
844            if let Some(&(zobrist, ref stones)) = group_info.get(&(id as u16)) {
845                assert_eq!(zobrist, group.zobrist);
846                assert_eq!(stones, &linked_stones);
847            } else {
848                assert_eq!(Zobrist::default(), group.zobrist);
849                assert!(linked_stones.is_empty());
850            }
851
852            if group.is_dead() {
853                expected_dead_groups.insert(id as u16);
854            }
855        }
856
857        // check dead groups
858        let linked_dead_groups = self.dead_groups.assert_valid_and_collect(&self.groups);
859        assert_eq!(expected_dead_groups, linked_dead_groups);
860
861        // check hash validness
862        let mut new_zobrist = Zobrist::default();
863        for tile in FlatTile::all(size) {
864            if let Some(player) = self.stone_at(tile) {
865                let value = Zobrist::for_color_tile(player, tile);
866                new_zobrist ^= value;
867            }
868        }
869        assert_eq!(self.zobrist, new_zobrist, "Invalid zobrist hash");
870
871        // check empty tiles linkedlist
872        let linked_empty_tiles = self.empty_list.assert_valid_and_collect(&self.tiles);
873        assert_eq!(empty_tiles, linked_empty_tiles);
874    }
875}
876
877impl Territory {
878    pub fn player(self) -> Option<Player> {
879        match self {
880            Territory::Stone(player) => Some(player),
881            Territory::Territory(player) => Some(player),
882            Territory::Neither | Territory::Both => None,
883        }
884    }
885
886    fn merge_empty(self, other: Territory) -> Territory {
887        let (left_a, left_b) = self.to_reach();
888        let (right_a, right_b) = other.to_reach();
889        Territory::from_reach(left_a || right_a, left_b || right_b)
890    }
891
892    fn to_reach(self) -> (bool, bool) {
893        match self {
894            Territory::Stone(Player::A) => (true, false),
895            Territory::Stone(Player::B) => (false, true),
896            Territory::Territory(Player::A) => (true, false),
897            Territory::Territory(Player::B) => (false, true),
898            Territory::Both => (true, true),
899            Territory::Neither => (false, false),
900        }
901    }
902
903    fn from_reach(reach_a: bool, reach_b: bool) -> Territory {
904        match (reach_a, reach_b) {
905            (false, false) => Territory::Neither,
906            (true, true) => Territory::Both,
907            (true, false) => Territory::Territory(Player::A),
908            (false, true) => Territory::Territory(Player::B),
909        }
910    }
911}
912
913// This is not a function on the struct because we need to use it while things are partially borrowed.
914fn change_liberty_edges_at(
915    size: u8,
916    tiles: &mut [TileContent],
917    groups: &mut [Group],
918    tile: FlatTile,
919    delta: i16,
920    skip_group_id: OptionU16,
921) {
922    for adj in tile.all_adjacent(size) {
923        if let Some(group_id) = tiles[adj.index() as usize].group_id.to_option() {
924            if OptionU16::Some(group_id) != skip_group_id {
925                let count = &mut groups[group_id as usize].liberty_edge_count;
926                *count = count.wrapping_add_signed(delta);
927            }
928        }
929    }
930}
931
932impl Group {
933    fn is_dead(&self) -> bool {
934        self.stones.is_empty()
935    }
936}
937
938impl Eq for Chains {}
939
940impl PartialEq for Chains {
941    fn eq(&self, other: &Self) -> bool {
942        // TODO see if this optimizes to something decent
943        self.tiles.len() == other.tiles.len()
944            && self.zobrist() == other.zobrist()
945            && FlatTile::all(self.size).all(|tile| self.stone_at(tile) == other.stone_at(tile))
946    }
947}
948
949impl Hash for Chains {
950    fn hash<H: Hasher>(&self, state: &mut H) {
951        self.zobrist().hash(state);
952    }
953}
954
955impl PlacementKind {
956    pub fn is_suicide(self) -> bool {
957        match self {
958            PlacementKind::Normal | PlacementKind::Capture => false,
959            PlacementKind::SuicideSingle | PlacementKind::SuicideMulti => true,
960        }
961    }
962
963    pub fn removes_existing_stones(self) -> bool {
964        match self {
965            PlacementKind::Normal | PlacementKind::SuicideSingle => false,
966            PlacementKind::Capture | PlacementKind::SuicideMulti => true,
967        }
968    }
969}
970
971impl Linked for TileContent {
972    fn link(&self) -> &LinkNode {
973        &self.link
974    }
975
976    fn link_mut(&mut self) -> &mut LinkNode {
977        &mut self.link
978    }
979}
980
981impl Linked for Group {
982    fn link(&self) -> &LinkNode {
983        &self.dead_link
984    }
985
986    fn link_mut(&mut self) -> &mut LinkNode {
987        &mut self.dead_link
988    }
989}