pachi_py/cypachi.pyx (263 lines of code) (raw):
import numpy as np
cimport numpy as np
cimport cython
from libc.stdlib cimport malloc, free
from libc.stdio cimport FILE
from libcpp.vector cimport vector
from libcpp.string cimport string
from cython.operator cimport dereference as deref
from cpython.ref cimport PyObject
##### Wrapper API declarations #####
# These must go first before Pachi imports, because they use extern "C" on Pachi headers
cdef extern from "ptr.hpp":
cppclass ptr[T]:
ptr()
T* get()
void reset(T*)
void assign(const ptr[T]&)
# Exceptions wrappers for C++ code
# see http://stackoverflow.com/a/29002414
class IllegalMove(RuntimeError):
pass
class PachiEngineError(RuntimeError):
pass
cdef public PyObject* _PyIllegalMove = <PyObject*> IllegalMove
cdef public PyObject* _PyPachiEngineError = <PyObject*> PachiEngineError
cdef extern from "exceptions.hpp":
void raise_py_error()
# C++ wrapper for Pachi
cdef extern from "goutil.hpp":
enum stone:
S_NONE
S_BLACK
S_WHITE
S_OFFBOARD
S_MAX
ctypedef int coord_t
cppclass PachiBoard:
board* pachiboard()
ptr[PachiBoard] clone()
int size()
ctypedef ptr[PachiBoard] PachiBoardPtr
PachiBoardPtr CreatePachiBoard(int size)
void GetLegalMoves(PachiBoardPtr b, stone color, bint filter_suicides, vector[coord_t]* out)
bint IsTerminal(PachiBoardPtr)
float FastScore(PachiBoardPtr)
float OfficialScore(PachiBoardPtr)
string ToString(PachiBoardPtr)
void PlayInPlace(PachiBoardPtr b, const move& m) except +raise_py_error
PachiBoardPtr Play(PachiBoardPtr, const move&) except +raise_py_error
PachiBoardPtr PlayRandom(PachiBoardPtr b, stone color, coord_t *coord)
# Coordinate representation conversion
# x/y are Pachi coordinates
# i/j are the encoding used for training
int i_from_xy(board* b, int x, int y)
int j_from_xy(board* b, int x, int y)
stone board_atij(board* b, int i, int j)
int coord_ij(board* b, int i, int j)
int i_from_coord(board* b, coord_t c)
int j_from_coord(board* b, coord_t c)
cppclass PachiEngine:
PachiEngine(PachiBoardPtr b, const string& engine_type, string arg) except +raise_py_error
PachiBoardPtr get_curr_board()
coord_t genmove(stone curr_color, const string& timestr) except +raise_py_error
void notify(coord_t move_coord, stone move_color)
##### Pachi API declarations #####
cdef extern from "stone.h":
enum stone:
S_NONE
S_BLACK
S_WHITE
S_OFFBOARD
S_MAX
stone str2stone(char *str)
stone pachi_stone_other "stone_other" (stone s)
cdef extern from "move.h":
ctypedef int coord_t
# void coord_done(coord_t *c)
# # Return coordinate string in a dynamically allocated buffer. Thread-safe.
char *coord2str(coord_t c, board *b)
coord_t *str2coord(char *str, int board_size)
struct move:
coord_t coord
stone color
# Special moves
coord_t pass_coord "pass"
coord_t resign_coord "resign"
cdef extern from "board.h":
int GROUP_REFILL_LIBS
struct group:
int libs
ctypedef coord_t group_t
struct board:
pass
int board_size(board*) # includes padding, so returns 19 + 2
# stone board_at(board* b, int c)
# stone board_atxy(board* b, int x, int y)
# group_t group_at(board* b, int c)
# group board_group_info(board* b, int g)
void board_handicap(board *board, int stones, FILE *f)
bint board_set_rules(board *board, char *name)
cdef extern from "random.h":
void fast_srandom(unsigned long seed)
# Internal utility functions
cdef enum Channel:
CHAN_CELL_BLACK = 0
CHAN_CELL_WHITE
CHAN_CELL_EMPTY
cdef int _NUM_FEATURE_CHANNELS = 3
cdef void encode_board(board* b, np.ndarray[np.int_t, ndim=3] out_x):
cdef int s = board_size(b) - 2
assert out_x.shape[0] == _NUM_FEATURE_CHANNELS and out_x.shape[1] == out_x.shape[2] == s
out_x[...] = 0
cdef unsigned int i, j
cdef stone curr_stone
for i in range(s):
for j in range(s):
curr_stone = board_atij(b, i, j)
# Cell color
if curr_stone == S_BLACK:
out_x[<unsigned int>CHAN_CELL_BLACK,i,j] = 1
elif curr_stone == S_WHITE:
out_x[<unsigned int>CHAN_CELL_WHITE,i,j] = 1
elif curr_stone == S_NONE:
out_x[<unsigned int>CHAN_CELL_EMPTY,i,j] = 1
else:
assert False
# Python board wrapper
cdef PyPachiBoard wrap_board(PachiBoardPtr b):
return PyPachiBoard()._set(b)
cdef class PyPachiBoard:
cdef PachiBoardPtr _bptr
cdef PachiBoard* _b # for convenience. set to self._bptr.get()
cdef int _size
cdef _set(self, PachiBoardPtr b):
self._bptr.assign(b)
self._b = self._bptr.get()
self._size = self._b.size()
return self
def __repr__(self):
return ToString(self._bptr)
def clone(self):
return wrap_board(self._bptr.get().clone())
def get_stones(self, stone color):
cdef np.ndarray[np.int_t, ndim=2] stones = np.empty((self._size*self._size, 2), dtype=np.int)
cdef int i, j
cdef unsigned int num_stones = 0
for i in range(self._size):
for j in range(self._size):
if board_atij(self._b.pachiboard(), i, j) == color:
stones[num_stones,0] = i
stones[num_stones,1] = j
num_stones += 1
return stones[:num_stones,:]
def get_legal_coords(self, stone color, bint filter_suicides=False):
cdef vector[coord_t] legal_coords
GetLegalMoves(self._bptr, color, filter_suicides, &legal_coords)
return legal_coords
def encode(self, np.ndarray[np.int_t, ndim=3] out_x=None):
if out_x is None:
out_x = np.zeros((NUM_FEATURE_CHANNELS, self._size, self._size), dtype=int)
encode_board(self._b.pachiboard(), out_x)
return out_x
def play(self, int coord, stone color):
assert color == S_BLACK or color == S_WHITE
cdef move m
m.color = color
m.coord = coord
return wrap_board(Play(self._bptr, m))
def play_random(self, stone color, bint return_action=False):
cdef coord_t c
cdef PyPachiBoard out = wrap_board(PlayRandom(self._bptr, color, &c))
if not return_action: return out
return out, c
def play_inplace(self, int coord, stone color):
assert color == S_BLACK or color == S_WHITE
cdef move m
m.color = color
m.coord = coord
PlayInPlace(self._bptr, m)
property size:
def __get__(self):
return self._size
property num_stones:
def __get__(self):
cdef int i, j, n = 0
for i in range(self._size):
for j in range(self._size):
if board_atij(self._b.pachiboard(), i, j) != S_NONE:
n += 1
return n
property empty:
def __get__(self):
return self.num_stones == 0
property black_stones:
def __get__(self):
return self.get_stones(S_BLACK)
property white_stones:
def __get__(self):
return self.get_stones(S_WHITE)
property is_terminal:
def __get__(self):
return IsTerminal(self._bptr)
property fast_score:
def __get__(self):
return FastScore(self._bptr)
property official_score:
def __get__(self):
return OfficialScore(self._bptr)
def __richcmp__(PyPachiBoard self, PyPachiBoard other, int op):
# TODO: use the C++ operator==
if op != 2 and op != 3: return NotImplemented
if other._size != self._size: return False
cdef int i, j
for i in range(self._size):
for j in range(self._size):
if board_atij(self._b.pachiboard(), i, j) != board_atij(other._b.pachiboard(), i, j):
return op == 3
return op == 2
def __hash__(self):
# Not sure if this is the best hash implementation, but it seems to work
cdef long h = 0
cdef int i, j
for i in range(self._size):
for j in range(self._size):
h = 101*h + board_atij(self._b.pachiboard(), i, j)
return h
def __getitem__(self, idx):
if len(idx) != 2:
raise IndexError('Must provide 2 indices')
cdef int i = idx[0]
cdef int j = idx[1]
if not (0 <= i < self._size and 0 <= j < self._size):
raise IndexError('Coordinates %d,%d out of range for board size %d' % (i, j, self._size))
return board_atij(self._b.pachiboard(), i, j)
### Utilities ###
def str_to_coord(self, char *s):
cdef coord_t *cptr = str2coord(s, board_size(self._b.pachiboard()))
cdef coord_t c = deref(cptr)
free(cptr)
# Sanity checking
if c == pass_coord or c == resign_coord: return c
if not (0 <= i_from_coord(self._b.pachiboard(), c) < self._size and
0 <= j_from_coord(self._b.pachiboard(), c) < self._size):
raise RuntimeError('Invalid coordinate %s' % s)
return c
def coord_to_str(self, coord_t c):
cdef char* tmpstr = coord2str(c, self._b.pachiboard())
cdef string s
s += tmpstr
free(tmpstr)
return s
def coord_to_ij(self, coord_t c):
cdef int i = i_from_coord(self._b.pachiboard(), c)
cdef int j = j_from_coord(self._b.pachiboard(), c)
return i, j
def ij_to_coord(self, int i, int j):
return coord_ij(self._b.pachiboard(), i, j)
cdef class PyPachiEngine:
cdef PachiEngine* _engine
def __cinit__(self, PyPachiBoard b, const string& engine_type, const string& arg):
self._engine = new PachiEngine(b._bptr, engine_type, arg)
def __dealloc__(self):
del self._engine
@property
def curr_board(self):
return wrap_board(self._engine.get_curr_board())
def genmove(self, stone curr_color, const string& timestr):
return self._engine.genmove(curr_color, timestr)
def notify(self, coord_t move_coord, stone move_color):
self._engine.notify(move_coord, move_color)
##### Exposed constants #####
NUM_FEATURE_CHANNELS = _NUM_FEATURE_CHANNELS
WHITE = S_WHITE
BLACK = S_BLACK
EMPTY = S_NONE
PASS_COORD = pass_coord
RESIGN_COORD = resign_coord
##### Exposed functions #####
cpdef PyPachiBoard CreateBoard(int size):
return wrap_board(CreatePachiBoard(size))
def pachi_srand(unsigned long seed):
fast_srandom(seed)
def stone_other(stone s):
return pachi_stone_other(s)
def color_to_str(stone s):
if s == S_BLACK: return "black"
elif s == S_WHITE: return "white"
return "INVALID"