def test_add_subtract(self): d1 = strategy.MoveCountDistribution([0.75, 0.25]) d2 = strategy.MoveCountDistribution([0.7, 0.2, 0.1]) np.testing.assert_allclose((d1 + d2).dist, [1.45, 0.45, 0.1]) np.testing.assert_allclose((d2 + d1).dist, [1.45, 0.45, 0.1]) np.testing.assert_allclose((d1 - d2).dist, [0.05, 0.05, -0.1]) np.testing.assert_allclose((d2 - d1).dist, [-0.05, -0.05, 0.1])
def test_expected_value(self): np.testing.assert_approx_equal( strategy.MoveCountDistribution([0.75, 0.25]).expected_value(), 0.25) np.testing.assert_approx_equal( strategy.MoveCountDistribution([0.0, 0.2, 0.8]).expected_value(), 1.8) np.testing.assert_approx_equal( strategy.MoveCountDistribution([0.0, 0.0, 1.0]).expected_value(), 2)
def test_trim_low_prob(self): np.testing.assert_allclose( strategy.MoveCountDistribution([0.1, 0.2, 0.3]).trim_low_prob(0.01).dist, [0.1, 0.2, 0.3]) np.testing.assert_allclose( strategy.MoveCountDistribution([0.1, 0.2, 0.3]).trim_low_prob(0.15).dist, [0.0, 0.2, 0.3]) np.testing.assert_allclose( strategy.MoveCountDistribution([0.3, 0.2, 0.1, 0.01]).trim_low_prob(0.15).dist, [0.3, 0.2])
def create_distribution_store_from_gnubg(gnubg_dir): """Creates a database in our format from the gnubg database. This uses a really dumb and inefficient strategy of calling 'bearoffdump' many times as separate processes. But it allows us to not tweak the gnubg code and not have to deal with the vagaries of their format (just some annoying text parsing) Args: gnubg_dir: source directory of gnubg with everyting compiled Returns: strategy.DistributionStore """ progress_interval = 500 config = board.GameConfiguration(15, 6) store = strategy.DistributionStore(config) start_time = time.time() # gnubg uses a 1 based index as an argument to # bearoffdump. However, it doesn't have the end state as a valid # index so we add that manually. store.distribution_map[config.min_board_id] = ( strategy.MoveCountDistribution([1])) for idx in range(1, config.num_valid_boards): completed_process = subprocess.run([ os.path.join(gnubg_dir, 'bearoffdump'), os.path.join(gnubg_dir, 'gnubg_os0.bd'), '-n', str(idx) ], universal_newlines=True, stdout=subprocess.PIPE, check=True) try: b, mcd = parse_gnubg_dump(config, completed_process.stdout) except ValueError as err: raise ValueError('For gnubg index {}: {}'.format(idx, err)) store.distribution_map[b.get_id()] = mcd if idx % progress_interval == 0: frac_complete = idx / config.num_valid_boards this_time = time.time() print("%d/%d %.1f%%, %fs elapsed, %fs estimated total" % (idx, config.num_valid_boards, frac_complete * 100, this_time - start_time, (this_time - start_time) / frac_complete), flush=True) return store
def parse_gnubg_dump(config, gnubg_str): """Parse the output of gnubg's "bearoffdump". This parses the "Opponent" move distribution from thet "Bearing off" column Args: config: board.GameConfiguration gnubg_str: string to parse Returns board.Board, strategy.MoveCountDistribution """ pos_id_str = None mcd = None parsing_mcd = False pos_id_re = re.compile(r'GNU Backgammon Position ID: ([A-Za-z0-9+/]*)') start_mcd_re = re.compile(r'^Rolls\s+Player\s+Opponent') mcd_line_re = re.compile(r'^\s*\d+\s+[\d\.]+\s+([\d\.]+)') for line in gnubg_str.splitlines(): if parsing_mcd: match = mcd_line_re.search(line) if match: value = float(match.group(1)) / 100.0 if not mcd: mcd = strategy.MoveCountDistribution([value]) else: mcd = mcd.append([value]) else: parsing_mcd = False elif start_mcd_re.search(line): parsing_mcd = True else: match = pos_id_re.search(line) if match: pos_id_str = match.group(1) if not pos_id_str: raise ValueError('Never found position id line') if not mcd: raise ValueError('Never found move distribution') return (board.Board.from_id(config, gnubg_id_str_to_board_id(config, pos_id_str)), mcd)
def testParse(self): input = """ Bearoff database: gnubg_os0.bd Position number : 30 Information about database: * On disk 1-sided bearoff database evaluator - generated by GNU Backgammon - up to 15 chequers on 6 points (54264 positions) per player - database includes gammon distributions Dump of position#: 30 GNU Backgammon Position ID: EwAAAAAAAAAAAA +13-14-15-16-17-18------19-20-21-22-23-24-+ | | | O O | OOO | | | O | OOO | | | | OO | | | | OO | | | | OO v| |BAR| | | | | | XXX | | | | XXX | | | | XXX | | | | XXX | | | | XXX +12-11-10--9--8--7-------6--5--4--3--2--1-+ Player Opponent Position 0 30 Bearing off Bearing at least one chequer off Rolls Player Opponent Player Opponent 0 100.000 0.000 100.000 100.000 1 0.000 13.889 0.000 0.000 2 0.000 86.111 Average rolls Bearing off Saving gammon Player Opponent Player Opponent Mean 0.000 1.861 0.000 0.000 Std dev 0.000 0.346 0.000 0.000 Effective pip count: Player Opponent EPC 0.000 15.199 Wastage 0.000 10.199 EPC = 8.167 * Average rolls Wastage = EPC - pips """ config = board.GameConfiguration(15, 6) b, mcd = gnubg_interface.parse_gnubg_dump(config, input) expected_b = board.Board(config, [12, 2, 0, 1, 0, 0, 0]) self.assertEqual(b, expected_b, msg='expected={}, got={}'.format(expected_b, b)) expected_mcd = strategy.MoveCountDistribution([0.0, .13889, .86111]) np.testing.assert_allclose(expected_mcd.dist, mcd.dist, atol=1e-5)
def test_append(self): d = strategy.MoveCountDistribution([0.1, 0.2]) np.testing.assert_allclose( d.append([0.3, 0.4]).dist, [0.1, 0.2, 0.3, 0.4])
def test_is_normalized(self): self.assertTrue( strategy.MoveCountDistribution([0.75, 0.25]).is_normalized()) self.assertFalse( strategy.MoveCountDistribution([0.1, 0.2]).is_normalized())
def test_increase_counts(self): d = strategy.MoveCountDistribution([0.75, 0.25]) np.testing.assert_allclose( d.increase_counts(2).dist, [0, 0, 0.75, 0.25])
def test_multiply_divide(self): d = strategy.MoveCountDistribution([0.75, 0.25]) np.testing.assert_allclose((d * 2).dist, [1.5, .5]) np.testing.assert_allclose((d / 5).dist, [.15, .05])
def test_init(self): d = strategy.MoveCountDistribution() np.testing.assert_allclose(d.dist, [0]) with self.assertRaises(ValueError): strategy.MoveCountDistribution([[0.1], [0.2]])