Skip to content

Commit 504c2c5

Browse files
committed
Merge pull request #131 from iopq/playout
Playout without self-atari of large chains
2 parents 2aa66a5 + 26f9f93 commit 504c2c5

File tree

15 files changed

+304
-288
lines changed

15 files changed

+304
-288
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(;FF[4]CA[UTF-8]AP[GoGui:1.4.9]
2+
KM[7.5]DT[2015-04-04]
3+
AB[ba][bb][cb][cd][be][cf][dg][ef][ci][bj][ck][dl][ek][fj][ei][dn][co][bp][cq][dr][eq][fp][eo][hc][ic][jc][kb][ka][gc][gb][ga][kd][ke][hd][he][hf][if][jf][ih][ij][kj][im][jl][km][ip][jo][kp][jq][nc][nb][ob][pc][od][ng][nf][of][pg][ph][oh][nj][oj][pj][pk][pl][ol][nl][nk]
4+
AW[ca][db][eb][ea][ce][df][cj][dk][ej][do][cp][dq][ep][ia][ja][jb][ha][hb][id][jd][je]
5+
)

src/board/mod.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,45 @@ impl Board {
563563
self.board[c.to_index(self.size)].chain_id = -1;
564564
self.board[c.to_index(self.size)].color = Empty;
565565
}
566+
567+
pub fn liberty_count(&self, c: Coord) -> usize {
568+
self.neighbours(c).iter().filter(|c| self.color(c) == Empty).count()
569+
}
570+
571+
pub fn removes_enemy_neighbouring_stones(&self, m: Move) -> usize {
572+
let enemy = m.color().opposite();
573+
self.neighbours(m.coord()).iter() //for all neighbours
574+
.filter(|c| { //take only
575+
self.color(c) == enemy && //the enemy stones
576+
self.get_chain(**c).unwrap().liberties().len() == 1 //that have 1 liberty
577+
})
578+
.count()
579+
}
580+
581+
pub fn new_chain_liberties(&self, m: Move) -> usize {
582+
let mut set: HashSet<Coord> = HashSet::new();
583+
584+
for &c in self.neighbours(m.coord()).iter() {
585+
if(self.color(&c) == *m.color()) {
586+
//add the liberties the chain
587+
for &liberty in self.get_chain(c).unwrap().liberties() {
588+
set.insert(liberty);
589+
}
590+
} else if(self.color(&c) == Empty) {
591+
set.insert(c);
592+
}
593+
};
594+
set.len() - 1 //minus the stone we're about to play
595+
}
596+
597+
//the length of all merged chains after the current move
598+
pub fn new_chain_length(&self, m: Move) -> usize {
599+
let set: HashSet<&Coord> = self.neighbours(m.coord()).iter()
600+
.filter(|c| self.color(c) == *m.color())
601+
.flat_map(|c| self.get_chain(*c).unwrap().coords().iter())
602+
.collect();
603+
set.len() + 1 //plus the stone we're about to play
604+
}
566605

567606
pub fn score(&self) -> Score {
568607
Score::new(self)

src/board/test/hypotheticals.rs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/************************************************************************
2+
* *
3+
* Copyright 2015 Igor Polyakov *
4+
* *
5+
* This file is part of Iomrascálaí. *
6+
* *
7+
* Iomrascálaí is free software: you can redistribute it and/or modify *
8+
* it under the terms of the GNU General Public License as published by *
9+
* the Free Software Foundation, either version 3 of the License, or *
10+
* (at your option) any later version. *
11+
* *
12+
* Iomrascálaí is distributed in the hope that it will be useful, *
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
15+
* GNU General Public License for more details. *
16+
* *
17+
* You should have received a copy of the GNU General Public License *
18+
* along with Iomrascálaí. If not, see <http://www.gnu.org/licenses/>. *
19+
* *
20+
************************************************************************/
21+
22+
#![cfg(test)]
23+
use std::path::Path;
24+
use board::Black;
25+
use board::Coord;
26+
use board::Pass;
27+
use board::White;
28+
use board::movement::Play;
29+
use sgf::Parser;
30+
31+
#[test]
32+
fn top_left_has_one_liberty() {
33+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
34+
let game = parser.game().unwrap();
35+
let board = game.board();
36+
assert_eq!(1, board.liberty_count(Coord::new(1,19)));
37+
}
38+
39+
#[test]
40+
fn one_below_top_left_has_two_liberties() {
41+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
42+
let game = parser.game().unwrap();
43+
let board = game.board();
44+
assert_eq!(2, board.liberty_count(Coord::new(1,18)));
45+
}
46+
47+
#[test]
48+
fn two_below_top_left_has_three_liberties() {
49+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
50+
let game = parser.game().unwrap();
51+
let board = game.board();
52+
assert_eq!(3, board.liberty_count(Coord::new(1,17)));
53+
}
54+
55+
#[test]
56+
fn first_square_surrounded_by_four_liberties_in_top_left() {
57+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
58+
let game = parser.game().unwrap();
59+
let board = game.board();
60+
assert_eq!(4, board.liberty_count(Coord::new(5,16)));
61+
}
62+
63+
#[test]
64+
fn removes_one_stone() {
65+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
66+
let game = parser.game().unwrap();
67+
let board = game.board();
68+
assert_eq!(1, board.removes_enemy_neighbouring_stones(Play(Black, 4, 19)));
69+
}
70+
71+
#[test]
72+
fn removes_two_stones() {
73+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
74+
let game = parser.game().unwrap();
75+
let board = game.board();
76+
assert_eq!(2, board.removes_enemy_neighbouring_stones(Play(Black, 4, 15)));
77+
}
78+
79+
#[test]
80+
fn removes_three_stones() {
81+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
82+
let game = parser.game().unwrap();
83+
let board = game.board();
84+
assert_eq!(3, board.removes_enemy_neighbouring_stones(Play(Black, 4, 10)));
85+
}
86+
87+
#[test]
88+
fn removes_four_stones() {
89+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
90+
let game = parser.game().unwrap();
91+
let board = game.board();
92+
assert_eq!(4, board.removes_enemy_neighbouring_stones(Play(Black, 4, 4)));
93+
}
94+
95+
#[test]
96+
fn removes_three_neighbours() {
97+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
98+
let game = parser.game().unwrap();
99+
let board = game.board();
100+
assert_eq!(3, board.removes_enemy_neighbouring_stones(Play(Black, 9, 18)));
101+
}
102+
103+
#[test]
104+
fn removes_two_neighbours() {
105+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
106+
let game = parser.game().unwrap();
107+
let board = game.board();
108+
assert_eq!(2, board.removes_enemy_neighbouring_stones(Play(Black, 9, 15)));
109+
}
110+
111+
#[test]
112+
fn two_stones_have_six_liberties() {
113+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
114+
let game = parser.game().unwrap();
115+
let board = game.board();
116+
117+
let play = Play(Black, 10, 12);
118+
assert_eq!(2, board.new_chain_length(play));
119+
assert_eq!(6, board.new_chain_liberties(play));
120+
}
121+
122+
#[test]
123+
fn three_stones_have_eight_liberties() {
124+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
125+
let game = parser.game().unwrap();
126+
let board = game.board();
127+
let play = Play(Black, 10, 10);
128+
assert_eq!(3, board.new_chain_length(play));
129+
assert_eq!(8, board.new_chain_liberties(play));
130+
}
131+
132+
#[test]
133+
fn four_stones_have_eight_liberties() {
134+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
135+
let game = parser.game().unwrap();
136+
let board = game.board();
137+
let play = Play(Black, 10, 7);
138+
assert_eq!(4, board.new_chain_length(play));
139+
assert_eq!(8, board.new_chain_liberties(play));
140+
}
141+
142+
#[test]
143+
fn five_stones_have_eight_liberties() {
144+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
145+
let game = parser.game().unwrap();
146+
let board = game.board();
147+
let play = Play(Black, 10, 4);
148+
assert_eq!(5, board.new_chain_length(play));
149+
assert_eq!(8, board.new_chain_liberties(play));
150+
}
151+
152+
#[test]
153+
fn six_stones_have_nine_liberties() {
154+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
155+
let game = parser.game().unwrap();
156+
let board = game.board();
157+
let play = Play(Black, 15, 17);
158+
assert_eq!(6, board.new_chain_length(play));
159+
assert_eq!(9, board.new_chain_liberties(play));
160+
}
161+
162+
#[test]
163+
fn seven_stones_have_ten_liberties() {
164+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
165+
let game = parser.game().unwrap();
166+
let board = game.board();
167+
let play = Play(Black, 15, 13);
168+
assert_eq!(7, board.new_chain_length(play));
169+
assert_eq!(10, board.new_chain_liberties(play));
170+
}
171+
172+
#[test]
173+
fn nine_stones_have_twelve_liberties() {
174+
let parser = Parser::from_path(Path::new("fixtures/sgf/hypothetical-plays.sgf"));
175+
let game = parser.game().unwrap();
176+
let board = game.board();
177+
let play = Play(Black, 15, 9);
178+
assert_eq!(9, board.new_chain_length(play));
179+
assert_eq!(12, board.new_chain_liberties(play));
180+
}

src/board/test/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use std::path::Path;
4242
mod diagonals;
4343
mod eye;
4444
mod ko;
45+
mod hypotheticals;
4546

4647
#[test]
4748
fn getting_a_valid_coord_returns_a_color() {

src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl Config {
3737
pub fn default() -> Config {
3838
Config {
3939
log: false,
40-
playout: Box::new(NoEyesPlayout::new()),
40+
playout: ::playout::factory(Some(String::from_str("default"))),
4141
ruleset: Minimal,
4242
threads: 1,
4343
uct_expand_after: 1,

src/engine/mc/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ fn spin_up_worker<'a, T: McEngine>(color: Color, recv_halt: Receiver<()>, moves:
125125
loop {
126126
for _ in 0..runs {
127127
let m = moves[rng.gen::<usize>() % moves.len()];
128-
let playout_result = config.playout.run(&board, Some(&m), &mut rng);
128+
let mut b = board.clone();
129+
let playout_result = config.playout.run(&mut b, Some(&m), &mut rng);
129130
let winner = playout_result.winner();
130131
T::record_playout(&mut stats, &playout_result, winner == color);
131132
}

src/engine/uct/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,13 @@ fn spin_up_worker<'a>(config: Arc<Config>, board: Board, color: Color, send_to_m
112112
_ = receive_halt.recv() => { break; },
113113
task = receive_from_main.recv() => {
114114
let (path, moves, expanded) = task.unwrap();
115-
// TODO: We do a clone here and then one when we
116-
// call run(). One is enough!
117115
let mut b = board.clone();
118116
for &m in moves.iter() {
119117
b.play_legal_move(m);
120118
}
121119
// Playout is smart enough to correctly handle the
122120
// case where the game is already over.
123-
let playout_result = config.playout.run(&b, None, &mut rng);
121+
let playout_result = config.playout.run(&mut b, None, &mut rng);
124122
let winner = playout_result.winner();
125123
let send_to_self = send_to_self.clone();
126124
send_to_main.send(((path, winner), send_to_self));

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub fn main() {
7474
opts.optopt("e", "engine", "select an engine (defaults to amaf)", "amaf|mc|random|uct");
7575
opts.optopt("r", "ruleset", "select the ruleset (defaults to chinese)", "cgos|chinese|tromp-taylor|minimal");
7676
opts.optopt("t", "threads", "number of threads to use (defaults to 1)", "NUM");
77-
opts.optopt("p", "playout", "type of playout to use (defaults to no-eyes)", "no-eyes|no-eyes-with-pass|simple|simple-with-pass");
77+
opts.optopt("p", "playout", "type of playout to use (defaults to light)", "light|no-self-atari");
7878
opts.optflag("h", "help", "print this help menu");
7979
opts.optflag("v", "version", "print the version number");
8080
opts.optflag("l", "log", "log to stderr (defaults to false)");

src/playout/mod.rs

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,61 +20,45 @@
2020
************************************************************************/
2121

2222
pub use self::no_eyes::NoEyesPlayout;
23-
pub use self::no_eyes::NoEyesWithPassPlayout;
24-
pub use self::simple::SimplePlayout;
25-
pub use self::simple::SimpleWithPassPlayout;
23+
pub use self::no_eyes::NoSelfAtariPlayout;
2624
use board::Board;
2725
use board::Color;
2826
use board::Move;
2927
use board::Pass;
3028
use board::Play;
3129

32-
//use rand::random;
3330
use rand::{Rng, XorShiftRng};
3431

3532
mod no_eyes;
36-
mod simple;
3733
mod test;
3834

3935
pub fn factory(opt: Option<String>) -> Box<Playout> {
40-
match opt {
41-
Some(s) => {
42-
match s.as_ref() {
43-
"no-eyes-with-pass" => Box::new(NoEyesWithPassPlayout::new()),
44-
"simple" => Box::new(SimplePlayout::new()),
45-
"simple-with-pass" => Box::new(SimpleWithPassPlayout::new()),
46-
_ => Box::new(NoEyesPlayout::new()),
47-
}
48-
},
49-
None => Box::new(NoEyesPlayout::new())
36+
match opt.as_ref().map(::std::ops::Deref::deref) {
37+
Some("no-self-atari") => Box::new(NoSelfAtariPlayout),
38+
_ => Box::new(NoEyesPlayout),
5039
}
5140
}
5241

5342
pub trait Playout: Sync + Send {
5443

55-
fn run(&self, b: &Board, initial_move: Option<&Move>, rng: &mut XorShiftRng) -> PlayoutResult {
56-
let mut board = b.clone();
44+
fn run(&self, board: &mut Board, initial_move: Option<&Move>, rng: &mut XorShiftRng) -> PlayoutResult {
5745
let mut played_moves = Vec::new();
58-
match initial_move {
59-
Some(&m) => {
60-
board.play(m);
61-
played_moves.push(m);
62-
},
63-
None => {}
64-
}
46+
47+
initial_move.map(|&m| {
48+
board.play_legal_move(m);
49+
played_moves.push(m);
50+
});
51+
6552
let max_moves = self.max_moves(board.size());
66-
let mut move_count = 0;
67-
while !board.is_game_over() && move_count < max_moves {
53+
while !board.is_game_over() && played_moves.len() < max_moves {
6854
let m = self.select_move(&board, rng);
6955
board.play_legal_move(m);
7056
played_moves.push(m);
71-
move_count += 1;
7257
}
7358
PlayoutResult::new(played_moves, board.winner())
7459
}
7560

7661
fn is_playable(&self, board: &Board, m: &Move) -> bool;
77-
fn include_pass(&self) -> bool;
7862

7963
fn max_moves(&self, size: u8) -> usize {
8064
size as usize * size as usize * 3
@@ -83,20 +67,14 @@ pub trait Playout: Sync + Send {
8367
fn select_move(&self, board: &Board, rng: &mut XorShiftRng) -> Move {
8468
let color = board.next_player();
8569
let vacant = board.vacant();
86-
let playable_move = vacant
70+
let playable_move = vacant
8771
.iter()
8872
.map(|c| Play(color, c.col, c.row))
8973
.position(|m| board.is_legal(m).is_ok() && self.is_playable(board, &m));
9074
if playable_move.is_some() {
9175
loop {
92-
let add = if self.include_pass() {
93-
1
94-
} else {
95-
0
96-
};
97-
9876
let first = playable_move.unwrap();
99-
let r = first + rng.gen::<usize>() % (vacant.len() + add - first);
77+
let r = first + rng.gen::<usize>() % (vacant.len() - first);
10078
if r == vacant.len() {
10179
return Pass(color);
10280
} else {
@@ -116,7 +94,6 @@ pub trait Playout: Sync + Send {
11694

11795
}
11896

119-
#[derive(Debug)]
12097
pub struct PlayoutResult {
12198
moves: Vec<Move>,
12299
winner: Color,

0 commit comments

Comments
 (0)