├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── betago.gif ├── betago ├── __init__.py ├── corpora │ ├── __init__.py │ ├── archive.py │ └── index.py ├── dataloader │ ├── README.md │ ├── __init__.py │ ├── base_processor.py │ ├── goboard.py │ ├── index_processor.py │ └── sampling.py ├── gosgf │ ├── __init__.py │ ├── sgf.py │ ├── sgf_grammar.py │ └── sgf_properties.py ├── gtp │ ├── __init__.py │ ├── board.py │ ├── command.py │ ├── frontend.py │ └── response.py ├── model.py ├── networks │ ├── __init__.py │ ├── large.py │ └── small.py ├── processor.py ├── scoring.py ├── simulate.py └── training │ ├── __init__.py │ ├── checkpoint.py │ └── kerashack.py ├── betagopy2-environmnet.yml ├── ear_reddening.png ├── examples ├── bot_vs_bot.py ├── end_to_end.py ├── play_gnugo.py ├── play_pachi.py ├── run_gtp.py ├── run_idiot_bot.py ├── train_and_store.py └── train_generator.py ├── model_zoo ├── 100_epochs_cnn_bot.yml ├── 100_epochs_cnn_weights.hd5 ├── README.md ├── demo_bot.yml ├── demo_weights.hd5 ├── one_epoch_cnn_bot.yml ├── one_epoch_cnn_weights.hd5 ├── ten_epochs_cnn_bot.yml └── ten_epochs_cnn_weights.hd5 ├── requirements.txt ├── run_demo.py ├── run_demo.sh ├── setup.cfg ├── setup.py ├── test_samples.py ├── tests ├── __init__.py ├── dataloader │ ├── __init__.py │ └── goboard_test.py ├── gosgf │ ├── __init__.py │ ├── sgf_grammar_test.py │ ├── sgf_properties_test.py │ └── sgf_test.py ├── gtp │ ├── __init__.py │ ├── board_test.py │ ├── command_test.py │ └── response_test.py ├── model_test.py └── scoring_test.py ├── train.py └── ui ├── Gruntfile.js ├── JGO ├── auto.js ├── board.js ├── canvas.js ├── constants.js ├── coordinate.js ├── index.js ├── node.js ├── notifier.js ├── record.js ├── setup.js ├── sgf.js ├── stones.js └── util.js ├── LICENSE.txt ├── README.md ├── demoBot.html ├── dist └── jgoboard-latest.js ├── large ├── black.png ├── board.js ├── shadow.png ├── shadow_dark.png ├── shinkaya.jpg ├── walnut.jpg └── white.png ├── main.js ├── medium ├── black.png ├── board.js ├── shadow.png ├── shadow_dark.png ├── shinkaya.jpg ├── walnut.jpg └── white.png └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | model_zoo/* linguist-documentation 2 | ui/* linguist-documentation 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # vim 9 | *.swp 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # Ignore data folders 63 | betago/data/* 64 | betago/dataloader/data/* 65 | examples/data/* 66 | data 67 | 68 | # KGS index 69 | *kgs_index.html 70 | 71 | 72 | # Hide test samples script 73 | betago/dataloader/test_samples.py 74 | betago/test_samples.py 75 | examples/test_samples.py 76 | 77 | *.ipynb_checkpoints 78 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: python 4 | python: 5 | - "2.7" 6 | - "3.4" 7 | before_install: 8 | - sudo apt-get install -y python-dev python-pip python-virtualenv gfortran libhdf5-dev pkg-config 9 | install: 10 | - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then 11 | wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; 12 | else 13 | wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 14 | fi 15 | - bash miniconda.sh -b -p $HOME/miniconda 16 | - export PATH="$HOME/miniconda/bin:$PATH" 17 | - hash -r 18 | - conda config --set always_yes yes --set changeps1 no 19 | - conda update -q conda 20 | - conda info -a 21 | - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION numpy scipy matplotlib pandas pytest h5py flask nose six tensorflow theano 22 | - source activate test-environment 23 | - pip install requests 24 | - python setup.py install 25 | script: nosetests --with-coverage 26 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Contributors 2 | ------------ 3 | * Max Pumperla 4 | * Kevin Ferguson 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Adapted from https://github.com/gw0/docker-keras-full 2 | # docker-debian-cuda - Debian 9 with CUDA Toolkit 3 | 4 | FROM gw000/keras:2.0.2-gpu 5 | MAINTAINER gw0 [http://gw.tnode.com/] 6 | 7 | # install py2-th-cpu (Python 2, Theano, CPU/GPU) 8 | ARG THEANO_VERSION=0.9.0 9 | ENV THEANO_FLAGS='device=cpu,floatX=float32' 10 | RUN pip --no-cache-dir install git+https://github.com/Theano/Theano.git@rel-${THEANO_VERSION} 11 | 12 | # install py3-tf-cpu/gpu (Python 3, TensorFlow, CPU/GPU) 13 | RUN apt-get update -qq \ 14 | && apt-get install --no-install-recommends -y \ 15 | # install python 3 16 | python3 \ 17 | python3-dev \ 18 | python3-pip \ 19 | python3-setuptools \ 20 | python3-virtualenv \ 21 | pkg-config \ 22 | # requirements for numpy 23 | libopenblas-base \ 24 | python3-numpy \ 25 | python3-scipy \ 26 | # requirements for keras 27 | python3-h5py \ 28 | python3-yaml \ 29 | python3-pydot \ 30 | && apt-get clean \ 31 | && rm -rf /var/lib/apt/lists/* 32 | 33 | ARG TENSORFLOW_VERSION=1.0.1 34 | ARG TENSORFLOW_DEVICE=gpu 35 | ARG TENSORFLOW_APPEND=_gpu 36 | RUN pip3 --no-cache-dir install https://storage.googleapis.com/tensorflow/linux/${TENSORFLOW_DEVICE}/tensorflow${TENSORFLOW_APPEND}-${TENSORFLOW_VERSION}-cp35-cp35m-linux_x86_64.whl 37 | 38 | ARG KERAS_VERSION=2.0.2 39 | ENV KERAS_BACKEND=tensorflow 40 | RUN pip3 --no-cache-dir install git+https://github.com/fchollet/keras.git@${KERAS_VERSION} 41 | 42 | # install py3-th-cpu/gpu (Python 3, Theano, CPU/GPU) 43 | ARG THEANO_VERSION=0.9.0 44 | ENV THEANO_FLAGS='device=cpu,floatX=float32' 45 | RUN pip3 --no-cache-dir install git+https://github.com/Theano/Theano.git@rel-${THEANO_VERSION} 46 | 47 | # install additional debian packages 48 | RUN apt-get update -qq \ 49 | && apt-get install --no-install-recommends -y \ 50 | # system tools 51 | less \ 52 | procps \ 53 | vim-tiny \ 54 | # build dependencies 55 | build-essential \ 56 | libffi-dev \ 57 | && apt-get clean \ 58 | && rm -rf /var/lib/apt/lists/* 59 | 60 | # Copy application to container 61 | RUN mkdir -p app 62 | WORKDIR /app 63 | COPY . /app 64 | 65 | # Install requirements 66 | RUN pip install -r requirements.txt 67 | 68 | # Expose default port and start app 69 | EXPOSE 8080 70 | ENTRYPOINT ["python", "run_demo.py"] 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Max Pumperla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./run_demo.py --port $PORT --host 0.0.0.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is now archived. It's been fun working on it, but it's time for me to move on. Thank you for all the support and feedback over the last couple of years. If someone is interested in taking ownership, let's discuss. :v: 2 | --- 3 | 4 | # BetaGo [![Build Status](https://travis-ci.org/maxpumperla/betago.svg?branch=master)](https://travis-ci.org/maxpumperla/betago) [![PyPI version](https://badge.fury.io/py/betago.svg)](https://badge.fury.io/py/betago) 5 | *So, you don't work at Google Deep Mind and you don't have access to Nature. You've come to the right place. BetaGo will stay beta! We are the 99%! We are Lee Sedol!* 6 | 7 | ![betago-demo](betago.gif) 8 | 9 | BetaGo lets you run your own Go engine. It downloads Go games for you, preprocesses them, trains a model on data, for instance a neural network using keras, and serves the trained model to an HTML front end, which you can use to play against your own Go bot. 10 | 11 | ## Getting started 12 | 13 | Test BetaGo by running the following commands. It should start a playable demo in your browser! This bot plays reasonable moves, but is still rather weak. 14 | 15 | ### Prerequisites 16 | 17 | #### Ubuntu/Debian 18 | ```{bash} 19 | sudo apt-get install -y python-dev python-pip python-virtualenv gfortran libhdf5-dev pkg-config liblapack-dev libblas-dev 20 | ``` 21 | #### Mac 22 | On a Mac we recommend using homebrew to install HDF5 first 23 | ```{bash} 24 | brew tap homebrew/science 25 | brew install hdf5 26 | ``` 27 | 28 | ### Installation 29 | ``` 30 | virtualenv .betago 31 | . .betago/bin/activate 32 | pip install --upgrade pip setuptools 33 | pip install betago 34 | git clone https://github.com/maxpumperla/betago 35 | cd betago 36 | python run_demo.py 37 | ``` 38 | 39 | ## Running betago with docker 40 | 41 | After installing docker and cloning betago, run the following commands to run the demo 42 | ```{bash} 43 | cd betago 44 | docker build -t betago . 45 | docker run -p 8080:8080 betago 46 | ``` 47 | 48 | ## Contribute 49 | You can modify and extend any of the steps outlined above and help decrease the gap between AlphaGo and BetaGo, tear down walls and disrupt the establishment. Consider contributing by: 50 | - Adding new models to the model zoo. 51 | - Writing new Go data processing functionality. 52 | - Adding more complex models and bots. 53 | 54 | ## How can I run my own bot? 55 | Training and serving a bot can be done in just a few steps. The following example uses a convolutional neural network implemented in keras, but you are free to choose other libraries as well. The code for this example can be found in the examples folder. 56 | We start by defining a Go data processor, which downloads and preprocesses Go games. A regular Go board consists of 19 times 19 fields. The ```SevenPlaneProcessor```, inspired by [1] loads seven planes of ```19*19``` data points, three layers representing moves of varying liberties for each color and one capturing for ko. 57 | ```{python} 58 | from betago.processor import SevenPlaneProcessor 59 | processor = SevenPlaneProcessor() 60 | input_channels = processor.num_planes 61 | 62 | # Load go data and one-hot encode labels 63 | X, y = processor.load_go_data(num_samples=1000) 64 | X = X.astype('float32') 65 | Y = np_utils.to_categorical(y, nb_classes) 66 | ``` 67 | Next, we train a neural network to predict moves. If you insist, you may call it a policy network. This example is just one of many possible architectures to tackle this problem and by no means optimal. Feel free to add or adapt layers and come up with your own experiments. We use the new Keras 1.0 here, but you could use older versions as well. 68 | 69 | ```{python} 70 | batch_size = 128 71 | nb_epoch = 20 72 | 73 | nb_classes = 19 * 19 # One class for each position on the board 74 | go_board_rows, go_board_cols = 19, 19 # input dimensions of go board 75 | nb_filters = 32 # number of convolutional filters to use 76 | nb_pool = 2 # size of pooling area for max pooling 77 | nb_conv = 3 # convolution kernel size 78 | 79 | # Specify a keras model with two convolutional layers and two dense layers, 80 | # connecting the (num_samples, 7, 19, 19) input to the 19*19 output vector. 81 | model = Sequential() 82 | model.add(Convolution2D(nb_filters, nb_conv, nb_conv, border_mode='valid', 83 | input_shape=(input_channels, go_board_rows, go_board_cols))) 84 | model.add(Activation('relu')) 85 | model.add(Convolution2D(nb_filters, nb_conv, nb_conv)) 86 | model.add(Activation('relu')) 87 | model.add(MaxPooling2D(pool_size=(nb_pool, nb_pool))) 88 | model.add(Dropout(0.2)) 89 | model.add(Flatten()) 90 | model.add(Dense(256)) 91 | model.add(Activation('relu')) 92 | model.add(Dropout(0.5)) 93 | model.add(Dense(nb_classes)) 94 | model.add(Activation('softmax')) 95 | model.compile(loss='categorical_crossentropy', 96 | optimizer='adadelta', 97 | metrics=['accuracy']) 98 | 99 | # Fit the model to data 100 | model.fit(X, Y, batch_size=batch_size, nb_epoch=nb_epoch, verbose=1) 101 | ``` 102 | 103 | With ```processor``` and ```model``` we can initialize a so called ```KerasBot```, which will serve the model for us. 104 | ```{python} 105 | import os 106 | import webbrowser 107 | # Open web frontend, assuming you cd'ed into betago 108 | webbrowser.open('file://' + os.getcwd() + '/ui/demoBot.html', new=2) 109 | 110 | # Create a bot from processor and model, then run it. 111 | from betago.model import KerasBot 112 | go_model = KerasBot(model=model, processor=processor) 113 | go_model.run() 114 | ``` 115 | 116 | 117 | ## Tell me how it works 118 | Alright, alright. BetaGo consists of a just few components, all of which you have already seen. First, to load and process data into memory, we use a ```GoDataProcessor```. BetaGo comes with two such processors out of the box, namely ```SevenPlaneProcessor``` and the simpler ```ThreePlaneProcessor``` but it's relatively straight forward to add new ones. The processor loads an index of zip files containing .sgf files with Go games and prepares them for further usage. There's a lot of Go games on KGS, so if you are not careful and try to load too many files this way, your application may crash. This is where ```GoFileProcessor``` comes in, which stores data in a lean, binary format to be picked up later on. The work on processors originated from @hughperkins kgsgo-dataset-preprocessor project, which deserves a lot of credit. 119 | 120 | Next, to actually predict moves on data processed by any of the above processors, we provide a default implementation of a ```GoModel```, called ```KerasBot```, which trains a deep network of your choice and exposes it to a Flask REST API, whereas ```IdiotBot``` simply makes random moves. ```KerasBot``` will try to place the best move, but will take inferior moves if predicted values turn out to be illegal moves. Notably, it is very handy to use keras here, but creating a new ```GoModel``` from scratch is not that hard. In particular, it should be possible to extend the simple approach of ```KerasBot``` to something more sophisticated, e.g. by borrowing ideas from AlphaGo and other approaches from the literature. 121 | 122 | The UI uses a fork of @jokkebk awesome jgoboard, and the current Go board front end is just a plain JavaScript client for the above Flask server. 123 | 124 | ## Motivation 125 | Being both a passionate and mediocre Go player and programmer, this project is a matter of honor to me. Also, I tried to get in touch with the AlphaGo team, as I'm very curious to hear what their AI has to say about the probability of the most famous of all Go moves, Shusaku's ear reddening move. Well, I never heard back from them, so I had to take matters into my own hands. Also, after white move 78 in game 4 of AlphaGo against Lee Sedol, the ear reddening move might even have lost its mythical number one position. Thanks again. Anyway, here you go: 126 | 127 | ![ear-reddening](ear_reddening.png) 128 | 129 | ## Literature 130 | [1] A. Clark, A. Storkey [Teaching Deep Convolutional Neural Networks to Play Go](http://arxiv.org/pdf/1412.3409v2.pdf). 131 | 132 | [2] C.J. Maddison, A. Huang, I. Sutskever, D. Silver [Move Evaluation in Go using Deep Neural Networks](http://arxiv.org/pdf/1412.6564v2.pdf) 133 | 134 | [3] D. Silver, A. Huang, C.J. Maddison, A. Guez, L. Sifre, G. van den Driessche, J. Schrittwieser, I. Antonoglou, V. Panneershelvam, M. Lanctot, S. Dieleman, D. Grewe, J. Nham, N. Kalchbrenner, I. Sutskever, T. Lillicrap, M. Leach, K. Kavukcuoglu, T. Graepel & D. Hassabis [Mastering the game of Go with deep neural networks and tree search](http://www.nature.com/nature/journal/v529/n7587/full/nature16961.html) 135 | -------------------------------------------------------------------------------- /betago.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/betago.gif -------------------------------------------------------------------------------- /betago/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/betago/__init__.py -------------------------------------------------------------------------------- /betago/corpora/__init__.py: -------------------------------------------------------------------------------- 1 | from .archive import * 2 | from .index import * 3 | -------------------------------------------------------------------------------- /betago/corpora/archive.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function 3 | import os 4 | import shutil 5 | import tarfile 6 | import tempfile 7 | from contextlib import contextmanager 8 | from operator import attrgetter 9 | 10 | try: 11 | cmp = cmp # Python 2 12 | except NameError: 13 | def cmp(a, b): # Python 3 14 | return (a > b) - (a < b) 15 | 16 | __all__ = [ 17 | 'SGF', 18 | 'find_sgfs', 19 | ] 20 | 21 | 22 | class SafetyError(Exception): 23 | pass 24 | 25 | 26 | class SGF(object): 27 | def __init__(self, locator, contents): 28 | self.locator = locator 29 | self.contents = contents 30 | 31 | def __str__(self): 32 | return 'SGF from %s' % self.locator 33 | 34 | 35 | class SGFLocator(object): 36 | # TODO Support zips and physical SGFs. 37 | def __init__(self, archive_path, archive_filename): 38 | self.archive_path = archive_path 39 | self.archive_filename = archive_filename 40 | 41 | @property 42 | def physical_file(self): 43 | return self.archive_path 44 | 45 | @property 46 | def game_file(self): 47 | return self.archive_filename 48 | 49 | def __cmp__(self, other): 50 | return cmp((self.physical_file, self.game_file), (other.physical_file, other.game_file)) 51 | 52 | def __str__(self): 53 | return '%s:%s' % (self.archive_path, self.archive_filename) 54 | 55 | def serialize(self): 56 | return { 57 | 'archive_path': self.archive_path, 58 | 'archive_filename': self.archive_filename, 59 | } 60 | 61 | @classmethod 62 | def deserialize(cls, data): 63 | return SGFLocator( 64 | archive_path=data['archive_path'], 65 | archive_filename=data['archive_filename'], 66 | ) 67 | 68 | 69 | class TarballSGFLocator(SGFLocator): 70 | def __init__(self, tarball_path, archive_filename): 71 | self.tarball_path = tarball_path 72 | self.archive_filename = archive_filename 73 | 74 | def __str__(self): 75 | return '%s:%s' % (self.tarball_path, self.archive_filename) 76 | 77 | def contents(self): 78 | return tarfile.open(self.tarball_path).extractfile(self.archive_filename).read() 79 | 80 | 81 | def find_sgfs(path): 82 | """Find all SGFs in a directory or archive.""" 83 | print(('Examining %s...' % (path,))) 84 | if os.path.isdir(path): 85 | return _walk_dir(path) 86 | if tarfile.is_tarfile(path): 87 | return _walk_tarball(path) 88 | 89 | 90 | def _walk_dir(path): 91 | children = os.listdir(path) 92 | children.sort() 93 | for child in children: 94 | full_path = os.path.join(path, child) 95 | for sgf in find_sgfs(full_path): 96 | yield sgf 97 | 98 | 99 | @contextmanager 100 | def tarball_iterator(tarball_path): 101 | tempdir = tempfile.mkdtemp(prefix='tmp-betago') 102 | tf = tarfile.open(tarball_path) 103 | 104 | # Check for unsafe filenames. Theoretically a tarball can contain 105 | # absolute filenames, or names like '../../whatever' 106 | def name_is_safe(filename): 107 | final_path = os.path.realpath(os.path.join(tempdir, filename)) 108 | dir_to_check = os.path.join(os.path.realpath(tempdir), '') 109 | return os.path.commonprefix([final_path, dir_to_check]) == dir_to_check 110 | if not all(name_is_safe(tf_entry.name) for tf_entry in tf): 111 | raise SafetyError('Tarball %s contains unsafe filenames' % (tarball_path,)) 112 | sgf_names = [tf_entry.name for tf_entry in tf 113 | if tf_entry.isfile and tf_entry.name.endswith('.sgf')] 114 | sgf_names.sort() 115 | tf.extractall(tempdir) 116 | try: 117 | yield [SGF(SGFLocator(tarball_path, sgf_name), open(os.path.join(tempdir, sgf_name)).read()) 118 | for sgf_name in sgf_names] 119 | finally: 120 | shutil.rmtree(tempdir) 121 | tf.close() 122 | 123 | 124 | def _walk_tarball(path): 125 | with tarball_iterator(path) as tarball: 126 | for sgf in tarball: 127 | yield sgf 128 | -------------------------------------------------------------------------------- /betago/corpora/index.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function 3 | import copy 4 | import itertools 5 | import json 6 | 7 | from .archive import SGFLocator, find_sgfs, tarball_iterator 8 | from ..dataloader.goboard import GoBoard 9 | from ..gosgf import Sgf_game 10 | from six.moves import range 11 | 12 | __all__ = [ 13 | 'CorpusIndex', 14 | 'build_index', 15 | 'load_index', 16 | 'store_index', 17 | ] 18 | 19 | 20 | def _sequence(game_record): 21 | """Extract game moves from a game record. 22 | 23 | The main sequence includes lots of stuff that is not actual game 24 | moves. 25 | """ 26 | seq = [] 27 | for item in game_record.get_main_sequence(): 28 | color, move = item.get_move() 29 | # color == None is entries that are not actual game play 30 | # move == None is a pass, which in theory we could try to 31 | # predict, but not yet 32 | if color is not None and move is not None: 33 | seq.append((color, move)) 34 | return seq 35 | 36 | 37 | class CorpusIndex(object): 38 | def __init__(self, physical_files, chunk_size, boundaries): 39 | self.physical_files = list(sorted(physical_files)) 40 | self.chunk_size = chunk_size 41 | self.boundaries = list(boundaries) 42 | 43 | @property 44 | def num_chunks(self): 45 | return len(self.boundaries) 46 | 47 | def serialize(self): 48 | return { 49 | 'physical_files': self.physical_files, 50 | 'chunk_size': self.chunk_size, 51 | 'boundaries': [boundary.serialize() for boundary in self.boundaries], 52 | } 53 | 54 | @classmethod 55 | def deserialize(cls, serialized): 56 | return cls( 57 | serialized['physical_files'], 58 | serialized['chunk_size'], 59 | [Pointer.deserialize(raw_boundary) for raw_boundary in serialized['boundaries']]) 60 | 61 | def get_chunk(self, chunk_number): 62 | assert 0 <= chunk_number < self.num_chunks 63 | chunk_start = self.boundaries[chunk_number] 64 | iterator = iter(self._generate_examples(chunk_start.locator)) 65 | # Skip to the appropriate move in the current game. 66 | for _ in range(chunk_start.position): 67 | next(iterator) 68 | return itertools.islice(iterator, self.chunk_size) 69 | 70 | def _generate_examples(self, start): 71 | """ 72 | Args: 73 | start (SGFLocator) 74 | """ 75 | start_file_idx = self.physical_files.index(start.physical_file) 76 | for physical_file in self.physical_files[start_file_idx:]: 77 | for sgf in self._generate_games(physical_file): 78 | if sgf.locator < start: 79 | continue 80 | board = GoBoard(19) 81 | try: 82 | game_record = Sgf_game.from_string(sgf.contents) 83 | # Set up the handicap. 84 | if game_record.get_handicap() > 0: 85 | for setup in game_record.get_root().get_setup_stones(): 86 | for move in setup: 87 | board.apply_move('b', move) 88 | for i, (color, move) in enumerate(_sequence(game_record)): 89 | yield copy.deepcopy(board), color, move 90 | if move is not None: 91 | board.apply_move(color, move) 92 | except ValueError: 93 | print(("Invalid SGF data, skipping game record %s" % (sgf,))) 94 | print(("Board was:\n%s" % (board,))) 95 | 96 | def _generate_games(self, physical_file): 97 | with tarball_iterator(physical_file) as tarball: 98 | for sgf in tarball: 99 | yield sgf 100 | 101 | 102 | class Pointer(object): 103 | """Identifies a specific training example inside a corpus.""" 104 | def __init__(self, locator, position): 105 | self.locator = locator 106 | self.position = position 107 | 108 | def __str__(self): 109 | return '%s:%d' % (self.locator, self.position) 110 | 111 | def serialize(self): 112 | return { 113 | 'locator': self.locator.serialize(), 114 | 'position': self.position, 115 | } 116 | 117 | @classmethod 118 | def deserialize(cls, serialized): 119 | return cls( 120 | SGFLocator.deserialize(serialized['locator']), 121 | serialized['position'] 122 | ) 123 | 124 | 125 | def build_index(path, chunk_size): 126 | """Index all SGF files found in the given location. 127 | 128 | This will include SGF that are contained inside zip or tar archives. 129 | """ 130 | physical_files = set() 131 | boundaries = [] 132 | examples_needed = 0 133 | for sgf in find_sgfs(path): 134 | physical_files.add(sgf.locator.physical_file) 135 | if examples_needed == 0: 136 | # The start of this SGF is a chunk boundary. 137 | boundaries.append(Pointer(sgf.locator, 0)) 138 | examples_needed = chunk_size 139 | game_record = Sgf_game.from_string(sgf.contents) 140 | num_positions = len(_sequence(game_record)) 141 | if examples_needed < num_positions: 142 | # The start of the next chunk is inside this SGF. 143 | boundaries.append(Pointer(sgf.locator, examples_needed)) 144 | remaining_examples = num_positions - examples_needed 145 | examples_needed = chunk_size - remaining_examples 146 | else: 147 | # This SGF is entirely contained within the current chunk. 148 | examples_needed -= num_positions 149 | 150 | return CorpusIndex(physical_files, chunk_size, boundaries) 151 | 152 | 153 | def load_index(input_stream): 154 | return CorpusIndex.deserialize(json.load(input_stream)) 155 | 156 | 157 | def store_index(index, output_stream): 158 | json.dump(index.serialize(), output_stream) 159 | -------------------------------------------------------------------------------- /betago/dataloader/README.md: -------------------------------------------------------------------------------- 1 | The dataloader module presented here is a rewrite of the work by @hughperkins found here: 2 | https://github.com/hughperkins/kgsgo-dataset-preprocessor 3 | 4 | We invested quite some time restructuring and adding components, but a lot of ideas and inspiration originate from the kgsgo-dataset-preprocessor project, which is why the relevant parts are published under MPL 2.0, as the original project was. 5 | -------------------------------------------------------------------------------- /betago/dataloader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/betago/dataloader/__init__.py -------------------------------------------------------------------------------- /betago/dataloader/goboard.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from __future__ import absolute_import 6 | import copy 7 | from six.moves import range 8 | 9 | 10 | class GoBoard(object): 11 | ''' 12 | Representation of a go board. It contains "GoStrings" to represent stones and liberties. Moreover, 13 | the board can account for ko and handle captured stones. 14 | ''' 15 | def __init__(self, board_size=19): 16 | ''' 17 | Parameters 18 | ---------- 19 | ko_last_move_num_captured: How many stones have been captured last move. If this is not 1, it can't be ko. 20 | ko_last_move: board position of the ko. 21 | board_size: Side length of the board, defaulting to 19. 22 | go_strings: Dictionary of go_string objects representing stones and liberties. 23 | ''' 24 | self.ko_last_move_num_captured = 0 25 | self.ko_last_move = -3 26 | self.board_size = board_size 27 | self.board = {} 28 | self.go_strings = {} 29 | 30 | def fold_go_strings(self, target, source, join_position): 31 | ''' Merge two go strings by joining their common moves''' 32 | if target == source: 33 | return 34 | for stone_position in source.stones.stones: 35 | self.go_strings[stone_position] = target 36 | target.insert_stone(stone_position) 37 | target.copy_liberties_from(source) 38 | target.remove_liberty(join_position) 39 | 40 | def add_adjacent_liberty(self, pos, go_string): 41 | ''' 42 | Append new liberties to provided GoString for the current move 43 | ''' 44 | row, col = pos 45 | if row < 0 or col < 0 or row > self.board_size - 1 or col > self.board_size - 1: 46 | return 47 | if pos not in self.board: 48 | go_string.insert_liberty(pos) 49 | 50 | def is_move_on_board(self, move): 51 | return move in self.board 52 | 53 | def is_move_suicide(self, color, pos): 54 | '''Check if a proposed move would be suicide.''' 55 | # Make a copy of ourself to apply the move. 56 | temp_board = copy.deepcopy(self) 57 | temp_board.apply_move(color, pos) 58 | new_string = temp_board.go_strings[pos] 59 | return new_string.get_num_liberties() == 0 60 | 61 | def is_move_legal(self, color, pos): 62 | '''Check if a proposed moved is legal.''' 63 | return (not self.is_move_on_board(pos)) and \ 64 | (not self.is_move_suicide(color, pos)) and \ 65 | (not self.is_simple_ko(color, pos)) 66 | 67 | def create_go_string(self, color, pos): 68 | ''' Create GoString from current Board and move ''' 69 | go_string = GoString(self.board_size, color) 70 | go_string.insert_stone(pos) 71 | self.go_strings[pos] = go_string 72 | self.board[pos] = color 73 | 74 | row, col = pos 75 | for adjpos in [(row - 1, col), (row + 1, col), (row, col - 1), (row, col + 1)]: 76 | self.add_adjacent_liberty(adjpos, go_string) 77 | return go_string 78 | 79 | def other_color(self, color): 80 | ''' 81 | Color of other player 82 | ''' 83 | if color == 'b': 84 | return 'w' 85 | if color == 'w': 86 | return 'b' 87 | 88 | def is_simple_ko(self, play_color, pos): 89 | ''' 90 | Determine ko from board position and player. 91 | 92 | Parameters: 93 | ----------- 94 | play_color: Color of the player to make the next move. 95 | pos: Current move as (row, col) 96 | ''' 97 | enemy_color = self.other_color(play_color) 98 | row, col = pos 99 | if self.ko_last_move_num_captured == 1: 100 | last_move_row, last_move_col = self.ko_last_move 101 | manhattan_distance_last_move = abs(last_move_row - row) + abs(last_move_col - col) 102 | if manhattan_distance_last_move == 1: 103 | last_go_string = self.go_strings.get((last_move_row, last_move_col)) 104 | if last_go_string is not None and last_go_string.get_num_liberties() == 1: 105 | if last_go_string.get_num_stones() == 1: 106 | num_adjacent_enemy_liberties = 0 107 | for adjpos in [(row - 1, col), (row + 1, col), (row, col - 1), (row, col + 1)]: 108 | if (self.board.get(adjpos) == enemy_color and 109 | self.go_strings[adjpos].get_num_liberties() == 1): 110 | num_adjacent_enemy_liberties = num_adjacent_enemy_liberties + 1 111 | if num_adjacent_enemy_liberties == 1: 112 | return True 113 | return False 114 | 115 | def check_enemy_liberty(self, play_color, enemy_pos, our_pos): 116 | ''' 117 | Update surrounding liberties on board after a move has been played. 118 | 119 | Parameters: 120 | ----------- 121 | play_color: Color of player about to move 122 | enemy_pos: latest enemy move 123 | our_pos: our latest move 124 | ''' 125 | enemy_row, enemy_col = enemy_pos 126 | our_row, our_col = our_pos 127 | 128 | # Sanity checks 129 | if enemy_row < 0 or enemy_row >= self.board_size or enemy_col < 0 or enemy_col >= self.board_size: 130 | return 131 | enemy_color = self.other_color(play_color) 132 | if self.board.get(enemy_pos) != enemy_color: 133 | return 134 | enemy_string = self.go_strings[enemy_pos] 135 | if enemy_string is None: 136 | raise ValueError('Inconsistency between board and go_strings at %r' % enemy_pos) 137 | 138 | # Update adjacent liberties on board 139 | enemy_string.remove_liberty(our_pos) 140 | if enemy_string.get_num_liberties() == 0: 141 | for enemy_pos in enemy_string.stones.stones: 142 | string_row, string_col = enemy_pos 143 | del self.board[enemy_pos] 144 | del self.go_strings[enemy_pos] 145 | self.ko_last_move_num_captured = self.ko_last_move_num_captured + 1 146 | for adjstring in [(string_row - 1, string_col), (string_row + 1, string_col), 147 | (string_row, string_col - 1), (string_row, string_col + 1)]: 148 | self.add_liberty_to_adjacent_string(adjstring, enemy_pos, play_color) 149 | 150 | def apply_move(self, play_color, pos): 151 | ''' 152 | Execute move for given color, i.e. play current stone on this board 153 | Parameters: 154 | ----------- 155 | play_color: Color of player about to move 156 | pos: Current move as (row, col) 157 | ''' 158 | if pos in self.board: 159 | raise ValueError('Move ' + str(pos) + 'is already on board.') 160 | 161 | self.ko_last_move_num_captured = 0 162 | row, col = pos 163 | 164 | # Remove any enemy stones that no longer have a liberty 165 | self.check_enemy_liberty(play_color, (row - 1, col), pos) 166 | self.check_enemy_liberty(play_color, (row + 1, col), pos) 167 | self.check_enemy_liberty(play_color, (row, col - 1), pos) 168 | self.check_enemy_liberty(play_color, (row, col + 1), pos) 169 | 170 | # Create a GoString for our new stone, and merge with any adjacent strings 171 | play_string = self.create_go_string(play_color, pos) 172 | play_string = self.fold_our_moves(play_string, play_color, (row - 1, col), pos) 173 | play_string = self.fold_our_moves(play_string, play_color, (row + 1, col), pos) 174 | play_string = self.fold_our_moves(play_string, play_color, (row, col - 1), pos) 175 | play_string = self.fold_our_moves(play_string, play_color, (row, col + 1), pos) 176 | 177 | # Store last move for ko 178 | self.ko_last_move = pos 179 | 180 | def add_liberty_to_adjacent_string(self, string_pos, liberty_pos, color): 181 | ''' Insert liberty into corresponding GoString ''' 182 | if self.board.get(string_pos) != color: 183 | return 184 | go_string = self.go_strings[string_pos] 185 | go_string.insert_liberty(liberty_pos) 186 | 187 | def fold_our_moves(self, first_string, color, pos, join_position): 188 | ''' Fold current board situation with a new move played by us''' 189 | row, col = pos 190 | if row < 0 or row >= self.board_size or col < 0 or col >= self.board_size: 191 | return first_string 192 | if self.board.get(pos) != color: 193 | return first_string 194 | string_to_fold = self.go_strings[pos] 195 | self.fold_go_strings(string_to_fold, first_string, join_position) 196 | return string_to_fold 197 | 198 | def __str__(self): 199 | result = 'GoBoard\n' 200 | for i in range(self.board_size - 1, -1, -1): 201 | line = '' 202 | for j in range(0, self.board_size): 203 | thispiece = self.board.get((i, j)) 204 | if thispiece is None: 205 | line = line + '.' 206 | if thispiece == 'b': 207 | line = line + '*' 208 | if thispiece == 'w': 209 | line = line + 'O' 210 | result = result + line + '\n' 211 | return result 212 | 213 | 214 | class BoardSequence(object): 215 | ''' 216 | Store a sequence of locations on a board, which could either represent stones or liberties. 217 | ''' 218 | def __init__(self, board_size=19): 219 | self.board_size = board_size 220 | self.stones = [] 221 | self.board = {} 222 | 223 | def insert(self, combo): 224 | row, col = combo 225 | if combo in self.board: 226 | return 227 | self.stones.append(combo) 228 | self.board[combo] = len(self.stones) - 1 229 | 230 | def erase(self, combo): 231 | if combo not in self.board: 232 | return 233 | iid = self.board[combo] 234 | if iid == len(self.stones) - 1: 235 | del self.stones[iid] 236 | del self.board[combo] 237 | return 238 | self.stones[iid] = self.stones[len(self.stones) - 1] 239 | del self.stones[len(self.stones) - 1] 240 | movedcombo = self.stones[iid] 241 | self.board[movedcombo] = iid 242 | del self.board[combo] 243 | 244 | def exists(self, combo): 245 | return combo in self.board 246 | 247 | def size(self): 248 | return len(self.stones) 249 | 250 | def __getitem__(self, iid): 251 | return self.stones[iid] 252 | 253 | def __str__(self): 254 | result = 'BoardSequence\n' 255 | for row in range(self.board_size - 1, -1, -1): 256 | thisline = "" 257 | for col in range(0, self.board_size): 258 | if self.exists((row, col)): 259 | thisline = thisline + "*" 260 | else: 261 | thisline = thisline + "." 262 | result = result + thisline + "\n" 263 | return result 264 | 265 | 266 | class GoString(object): 267 | ''' 268 | Represents a string of contiguous stones of one color on the board, including a list of all its liberties. 269 | ''' 270 | def __init__(self, board_size, color): 271 | self.board_size = board_size 272 | self.color = color 273 | self.liberties = BoardSequence(board_size) 274 | self.stones = BoardSequence(board_size) 275 | 276 | def get_stone(self, index): 277 | return self.stones[index] 278 | 279 | def get_liberty(self, index): 280 | return self.liberties[index] 281 | 282 | def insert_stone(self, combo): 283 | self.stones.insert(combo) 284 | 285 | def get_num_stones(self): 286 | return self.stones.size() 287 | 288 | def remove_liberty(self, combo): 289 | self.liberties.erase(combo) 290 | 291 | def get_num_liberties(self): 292 | return self.liberties.size() 293 | 294 | def insert_liberty(self, combo): 295 | self.liberties.insert(combo) 296 | 297 | def copy_liberties_from(self, source): 298 | for libertyPos in source.liberties.stones: 299 | self.liberties.insert(libertyPos) 300 | 301 | def __str__(self): 302 | result = "go_string[ stones=" + str(self.stones) + " liberties=" + str(self.liberties) + " ]" 303 | return result 304 | 305 | 306 | def from_string(board_string): 307 | """Build a board from an ascii-art representation. 308 | 309 | 'b' for black stones 310 | 'w' for white stones 311 | '.' for empty 312 | 313 | The bottom row is row 0, and the top row is row boardsize - 1. This 314 | matches the normal way you'd use board coordinates, with A1 in the 315 | bottom-left. 316 | 317 | Rows are separated by newlines. Extra whitespace is ignored. 318 | """ 319 | rows = [line.strip() for line in board_string.strip().split("\n")] 320 | boardsize = len(rows) 321 | if any(len(row) != boardsize for row in rows): 322 | raise ValueError('Board must be square') 323 | 324 | board = GoBoard(boardsize) 325 | rows.reverse() 326 | for r, row_string in enumerate(rows): 327 | for c, point in enumerate(row_string): 328 | if point in ('b', 'w'): 329 | board.apply_move(point, (r, c)) 330 | return board 331 | 332 | 333 | def to_string(board): 334 | """Make an ascii-art representation of a board.""" 335 | rows = [] 336 | for r in range(board.board_size): 337 | row = '' 338 | for c in range(board.board_size): 339 | row += board.board.get((r, c), '.') 340 | rows.append(row) 341 | rows.reverse() 342 | return '\n'.join(rows) 343 | -------------------------------------------------------------------------------- /betago/dataloader/index_processor.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at http://mozilla.org/MPL/2.0/. 4 | from __future__ import print_function 5 | from __future__ import absolute_import 6 | import os 7 | import sys 8 | import multiprocessing 9 | import six 10 | if sys.version_info[0] == 3: 11 | from urllib.request import urlopen, urlretrieve 12 | else: 13 | from urllib import urlopen, urlretrieve 14 | 15 | 16 | def worker(url_and_target): 17 | ''' 18 | Parallelize data download via multiprocessing 19 | ''' 20 | try: 21 | (url, target_path) = url_and_target 22 | print('>>> Downloading ' + target_path) 23 | urlretrieve(url, target_path) 24 | except (KeyboardInterrupt, SystemExit): 25 | print('>>> Exiting child process') 26 | 27 | 28 | class KGSIndex(object): 29 | 30 | def __init__(self, 31 | kgs_url='http://u-go.net/gamerecords/', 32 | index_page='kgs_index.html', 33 | data_directory='data'): 34 | ''' 35 | Create an index of zip files containing SGF data of actual Go Games on KGS. 36 | 37 | Parameters: 38 | ----------- 39 | kgs_url: URL with links to zip files of games 40 | index_page: Name of local html file of kgs_url 41 | data_directory: name of directory relative to current path to store SGF data 42 | ''' 43 | self.kgs_url = kgs_url 44 | self.index_page = index_page 45 | self.data_directory = data_directory 46 | self.file_info = [] 47 | self.urls = [] 48 | self.load_index() # Load index on creation 49 | 50 | def download_files(self): 51 | ''' 52 | Download zip files by distributing work on all available CPUs 53 | ''' 54 | if not os.path.isdir(self.data_directory): 55 | os.makedirs(self.data_directory) 56 | 57 | urls_to_download = [] 58 | for file_info in self.file_info: 59 | url = file_info['url'] 60 | file_name = file_info['filename'] 61 | if not os.path.isfile(self.data_directory + '/' + file_name): 62 | urls_to_download.append((url, self.data_directory + '/' + file_name)) 63 | cores = multiprocessing.cpu_count() 64 | pool = multiprocessing.Pool(processes=cores) 65 | try: 66 | it = pool.imap(worker, urls_to_download) 67 | for i in it: 68 | pass 69 | pool.close() 70 | pool.join() 71 | except KeyboardInterrupt: 72 | print(">>> Caught KeyboardInterrupt, terminating workers") 73 | pool.terminate() 74 | pool.join() 75 | sys.exit(-1) 76 | 77 | def create_index_page(self): 78 | ''' 79 | If there is no local html containing links to files, create one. 80 | ''' 81 | if os.path.isfile(self.index_page): 82 | print('>>> Reading cached index page') 83 | index_file = open(self.index_page, 'r') 84 | index_contents = index_file.read() 85 | index_file.close() 86 | else: 87 | print('>>> Downloading index page') 88 | fp = urlopen(self.kgs_url) 89 | data = six.text_type(fp.read()) 90 | fp.close() 91 | index_contents = data 92 | index_file = open(self.index_page, 'w') 93 | index_file.write(index_contents) 94 | index_file.close() 95 | return index_contents 96 | 97 | def load_index(self): 98 | ''' 99 | Create the actual index representation from the previously downloaded or cached html. 100 | ''' 101 | index_contents = self.create_index_page() 102 | split_page = [item for item in index_contents.split('Download')[0] 105 | if download_url.endswith('.tar.gz'): 106 | self.urls.append(download_url) 107 | for url in self.urls: 108 | filename = os.path.basename(url) 109 | split_file_name = filename.split('-') 110 | num_games = int(split_file_name[len(split_file_name) - 2]) 111 | print(filename + ' ' + str(num_games)) 112 | self.file_info.append({'url': url, 'filename': filename, 'num_games': num_games}) 113 | 114 | 115 | if __name__ == '__main__': 116 | index = KGSIndex() 117 | index.download_files() 118 | -------------------------------------------------------------------------------- /betago/dataloader/sampling.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at http://mozilla.org/MPL/2.0/. 4 | from __future__ import print_function 5 | from __future__ import absolute_import 6 | import os 7 | import random 8 | from .index_processor import KGSIndex 9 | from six.moves import range 10 | 11 | 12 | class Sampler(object): 13 | ''' 14 | Sample training and test data from zipped sgf files such that test data is kept stable. 15 | ''' 16 | 17 | def __init__(self, data_dir='data', num_test_games=100, cap_year=2015, seed=1337): 18 | self.data_dir = data_dir 19 | self.num_test_games = num_test_games 20 | self.test_games = [] 21 | self.train_games = [] 22 | self.test_folder = 'test_samples.py' 23 | self.cap_year = cap_year 24 | 25 | random.seed(seed) 26 | self.compute_test_samples() 27 | 28 | def draw_samples(self, num_sample_games): 29 | ''' 30 | Draw num_sample_games many training games from index. 31 | ''' 32 | available_games = [] 33 | index = KGSIndex(data_directory=self.data_dir) 34 | 35 | for fileinfo in index.file_info: 36 | filename = fileinfo['filename'] 37 | year = int(filename.split('-')[1].split('_')[0]) 38 | if year > self.cap_year: 39 | continue 40 | num_games = fileinfo['num_games'] 41 | for i in range(num_games): 42 | available_games.append((filename, i)) 43 | print('>>> Total number of games used: ' + str(len(available_games))) 44 | 45 | sample_set = set() 46 | while len(sample_set) < num_sample_games: 47 | sample = random.choice(available_games) 48 | if sample not in sample_set: 49 | sample_set.add(sample) 50 | print('Drawn ' + str(num_sample_games) + ' samples:') 51 | return list(sample_set) 52 | 53 | def draw_training_games(self): 54 | ''' 55 | Get list of all non-test games, that are no later than dec 2014 56 | Ignore games after cap_year to keep training data stable 57 | ''' 58 | index = KGSIndex(data_directory=self.data_dir) 59 | for file_info in index.file_info: 60 | filename = file_info['filename'] 61 | year = int(filename.split('-')[1].split('_')[0]) 62 | if year > self.cap_year: 63 | continue 64 | numgames = file_info['num_games'] 65 | for i in range(numgames): 66 | sample = (filename, i) 67 | if sample not in self.test_games: 68 | self.train_games.append(sample) 69 | print('total num training games: ' + str(len(self.train_games))) 70 | 71 | def compute_test_samples(self): 72 | ''' 73 | If not already existing, create local file to store fixed set of test samples 74 | ''' 75 | if not os.path.isfile(self.test_folder): 76 | test_games = self.draw_samples(self.num_test_games) 77 | test_sample_file = open(self.test_folder, 'w') 78 | for sample in test_games: 79 | test_sample_file.write(str(sample) + "\n") 80 | test_sample_file.close() 81 | 82 | test_sample_file = open(self.test_folder, 'r') 83 | sample_contents = test_sample_file.read() 84 | test_sample_file.close() 85 | for line in sample_contents.split('\n'): 86 | if line != "": 87 | (filename, index) = eval(line) 88 | self.test_games.append((filename, index)) 89 | 90 | def draw_training_samples(self, num_sample_games): 91 | ''' 92 | Draw training games, not overlapping with any of the test games. 93 | ''' 94 | available_games = [] 95 | index = KGSIndex(data_directory=self.data_dir) 96 | for fileinfo in index.file_info: 97 | filename = fileinfo['filename'] 98 | year = int(filename.split('-')[1].split('_')[0]) 99 | if year > self.cap_year: 100 | continue 101 | numgames = fileinfo['num_games'] 102 | for i in range(numgames): 103 | available_games.append((filename, i)) 104 | print('total num games: ' + str(len(available_games))) 105 | 106 | sample_set = set() 107 | while len(sample_set) < num_sample_games: 108 | sample = random.choice(available_games) 109 | if sample not in self.test_games: 110 | sample_set.add(sample) 111 | print('Drawn ' + str(num_sample_games) + ' samples:') 112 | return list(sample_set) 113 | 114 | def draw_all_training(self): 115 | ''' 116 | Draw all available training games. 117 | ''' 118 | available_games = [] 119 | index = KGSIndex(data_directory=self.data_dir) 120 | 121 | for fileinfo in index.file_info: 122 | filename = fileinfo['filename'] 123 | year = int(filename.split('-')[1].split('_')[0]) 124 | if year > self.cap_year: 125 | continue 126 | numgames = fileinfo['numGames'] 127 | for i in range(numgames): 128 | available_games.append((filename, i)) 129 | print('total num games: ' + str(len(available_games))) 130 | 131 | sample_set = set() 132 | for sample in available_games: 133 | if sample not in self.test_samples: 134 | sample_set.add(sample) 135 | print('Drawn all samples, ie ' + str(len(sample_set)) + ' samples:') 136 | return list(sample_set) 137 | -------------------------------------------------------------------------------- /betago/gosgf/__init__.py: -------------------------------------------------------------------------------- 1 | from .sgf import * 2 | -------------------------------------------------------------------------------- /betago/gtp/__init__.py: -------------------------------------------------------------------------------- 1 | from .frontend import * 2 | -------------------------------------------------------------------------------- /betago/gtp/board.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'coords_to_gtp_position', 3 | 'gtp_position_to_coords', 4 | ] 5 | 6 | # 'I' is intentionally omitted. 7 | COLS = 'ABCDEFGHJKLMNOPQRST' 8 | 9 | 10 | def coords_to_gtp_position(coords): 11 | """Convert (row, col) tuple to GTP board locations. 12 | 13 | Example: 14 | >>> coords_to_gtp_position((0, 0)) 15 | 'A1' 16 | """ 17 | row, col = coords 18 | # coords are zero-indexed, GTP is 1-indexed. 19 | return COLS[col] + str(row + 1) 20 | 21 | 22 | def gtp_position_to_coords(gtp_position): 23 | """Convert a GTP board location to a (row, col) tuple. 24 | 25 | Example: 26 | >>> gtp_position_to_coords('A1') 27 | (0, 0) 28 | """ 29 | col_str, row_str = gtp_position[0], gtp_position[1:] 30 | return (int(row_str) - 1, COLS.find(col_str)) 31 | -------------------------------------------------------------------------------- /betago/gtp/command.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'Command', 3 | ] 4 | 5 | 6 | class Command(object): 7 | """A GTP command. 8 | 9 | A GTP command contains: 10 | - An optional sequence number (used for matching up responses with 11 | commands) 12 | - A command name, e.g. 'genmove' 13 | - One or more arguments to the command, e.g. 'black' 14 | """ 15 | def __init__(self, sequence, name, args): 16 | self.sequence = sequence 17 | self.name = name 18 | self.args = tuple(args) 19 | 20 | def __eq__(self, other): 21 | return self.sequence == other.sequence and \ 22 | self.name == other.name and \ 23 | self.args == other.args 24 | 25 | def __repr__(self): 26 | return 'Command(%r, %r, %r)' % (self.sequence, self.name, self.args) 27 | 28 | def __str__(self): 29 | return repr(self) 30 | 31 | 32 | def parse(command_string): 33 | """Parse a GTP protocol line into a Command object. 34 | 35 | Example: 36 | >>> parse('999 play white D4') 37 | Command(999, 'play', ('white', 'D4')) 38 | """ 39 | pieces = command_string.split() 40 | # Check for the sequence number. 41 | try: 42 | sequence = int(pieces[0]) 43 | pieces = pieces[1:] 44 | except ValueError: 45 | # The first piece was non-numeric, so there was no sequence 46 | # number. 47 | sequence = None 48 | name, args = pieces[0], pieces[1:] 49 | return Command(sequence, name, args) 50 | -------------------------------------------------------------------------------- /betago/gtp/frontend.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import sys 3 | 4 | from . import command, response 5 | from .board import gtp_position_to_coords, coords_to_gtp_position 6 | from ..dataloader.goboard import GoBoard 7 | 8 | __all__ = [ 9 | 'GTPFrontend', 10 | ] 11 | 12 | # Fixed handicap placement as defined in GTP spec. 13 | HANDICAP_STONES = ['D4', 'Q16', 'D16', 'Q4', 'D10', 'Q10', 'K4', 'K16', 'K10'] 14 | 15 | 16 | class GTPFrontend(object): 17 | """Go Text Protocol frontend for a bot. 18 | 19 | Handles parsing GTP commands and formatting responses. 20 | 21 | Extremely limited implementation right now: 22 | - Only supports 19x19 boards. 23 | - Only supports fixed handicaps. 24 | - When white passes, black will pass too. 25 | """ 26 | 27 | def __init__(self, bot): 28 | self.bot = bot 29 | self._input = sys.stdin 30 | self._output = sys.stdout 31 | self._stopped = False 32 | 33 | def run(self): 34 | while not self._stopped: 35 | ln = self._input.readline().strip() 36 | cmd = command.parse(ln) 37 | resp = self.process(cmd) 38 | self._output.write(response.serialize(cmd, resp)) 39 | self._output.flush() 40 | 41 | def process(self, command): 42 | handlers = { 43 | 'boardsize': self.handle_boardsize, 44 | 'clear_board': self.handle_clear_board, 45 | 'fixed_handicap': self.handle_fixed_handicap, 46 | 'genmove': self.handle_genmove, 47 | 'known_command': self.handle_known_command, 48 | 'komi': self.ignore, 49 | 'play': self.handle_play, 50 | 'protocol_version': self.handle_protocol_version, 51 | 'quit': self.handle_quit, 52 | } 53 | handler = handlers.get(command.name, self.handle_unknown) 54 | resp = handler(*command.args) 55 | return resp 56 | 57 | def ignore(self, *args): 58 | """Placeholder for commands we haven't dealt with yet.""" 59 | return response.success() 60 | 61 | def handle_clear_board(self): 62 | self.bot.set_board(GoBoard()) 63 | return response.success() 64 | 65 | def handle_known_command(self, command_name): 66 | # TODO Should actually check if the command is known. 67 | return response.success('false') 68 | 69 | def handle_play(self, player, move): 70 | color = 'b' if player == 'black' else 'w' 71 | if move != 'pass': 72 | self.bot.apply_move(color, gtp_position_to_coords(move)) 73 | return response.success() 74 | 75 | def handle_genmove(self, player): 76 | bot_color = 'b' if player == 'black' else 'w' 77 | move = self.bot.select_move(bot_color) 78 | if move is None: 79 | return response.success('pass') 80 | return response.success(coords_to_gtp_position(move)) 81 | 82 | def handle_boardsize(self, size): 83 | if int(size) != 19: 84 | return response.error('Only 19x19 currently supported') 85 | return response.success() 86 | 87 | def handle_quit(self): 88 | self._stopped = True 89 | return response.success() 90 | 91 | def handle_unknown(self, *args): 92 | return response.error('Unrecognized command') 93 | 94 | def handle_fixed_handicap(self, nstones): 95 | nstones = int(nstones) 96 | for stone in HANDICAP_STONES[:nstones]: 97 | self.bot.apply_move('b', gtp_position_to_coords(stone)) 98 | return response.success() 99 | 100 | def handle_protocol_version(self): 101 | return response.success('2') 102 | -------------------------------------------------------------------------------- /betago/gtp/response.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'Response', 3 | 'error', 4 | 'serialize', 5 | 'success', 6 | ] 7 | 8 | 9 | class Response(object): 10 | """Response to a GTP command.""" 11 | def __init__(self, status, body): 12 | self.success = status 13 | self.body = body 14 | 15 | 16 | def success(body=''): 17 | """Make a successful GTP response.""" 18 | return Response(status=True, body=body) 19 | 20 | 21 | def error(body=''): 22 | """Make an error GTP response.""" 23 | return Response(status=False, body=body) 24 | 25 | 26 | def serialize(gtp_command, gtp_response): 27 | """Serialize a GTP response as a string. 28 | 29 | Needs the command we are responding to so we can match the sequence 30 | number. 31 | """ 32 | return '%s%s %s\n\n' % ( 33 | '=' if gtp_response.success else '?', 34 | '' if gtp_command.sequence is None else str(gtp_command.sequence), 35 | gtp_response.body, 36 | ) 37 | -------------------------------------------------------------------------------- /betago/model.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function 3 | import copy 4 | import random 5 | from itertools import chain, product 6 | from multiprocessing import Process 7 | 8 | from flask import Flask, request, jsonify 9 | from flask_cors import CORS 10 | import numpy as np 11 | from . import scoring 12 | from .dataloader.goboard import GoBoard 13 | from .processor import ThreePlaneProcessor 14 | from six.moves import range 15 | 16 | 17 | class HTTPFrontend(object): 18 | ''' 19 | HTTPFrontend is a simple Flask app served on localhost:8080, exposing a REST API to predict 20 | go moves. 21 | ''' 22 | 23 | def __init__(self, bot, graph, port=8080): 24 | self.bot = bot 25 | self.graph = graph 26 | self.port = port 27 | 28 | def start_server(self): 29 | ''' Start Go model server ''' 30 | self.server = Process(target=self.start_service) 31 | self.server.start() 32 | 33 | def stop_server(self): 34 | ''' Terminate Go model server ''' 35 | self.server.terminate() 36 | self.server.join() 37 | 38 | def run(self): 39 | ''' Run flask app''' 40 | app = Flask(__name__) 41 | CORS(app, resources={r"/prediction/*": {"origins": "*"}}) 42 | self.app = app 43 | 44 | @app.route('/dist/') 45 | def static_file_dist(path): 46 | return open("ui/dist/" + path).read() 47 | 48 | @app.route('/large/') 49 | def static_file_large(path): 50 | return open("ui/large/" + path).read() 51 | 52 | @app.route('/') 53 | def home(): 54 | # Inject game data into HTML 55 | board_init = 'initialBoard = ""' # backup variable 56 | board = {} 57 | for row in range(19): 58 | board_row = {} 59 | for col in range(19): 60 | # Get the cell value 61 | cell = str(self.bot.go_board.board.get((col, row))) 62 | # Replace values with numbers 63 | # Value will be be 'w' 'b' or None 64 | cell = cell.replace("None", "0") 65 | cell = cell.replace("b", "1") 66 | cell = cell.replace("w", "2") 67 | # Add cell to row 68 | board_row[col] = int(cell) # must be an int 69 | # Add row to board 70 | board[row] = board_row 71 | board_init = str(board) # lazy convert list to JSON 72 | 73 | return open("ui/demoBot.html").read().replace('"__i__"', 'var boardInit = ' + board_init) # output the modified HTML file 74 | 75 | @app.route('/sync', methods=['GET', 'POST']) 76 | def exportJSON(): 77 | export = {} 78 | export["hello"] = "yes?" 79 | return jsonify(**export) 80 | 81 | @app.route('/prediction', methods=['GET', 'POST']) 82 | def next_move(): 83 | '''Predict next move and send to client. 84 | 85 | Parses the move and hands the work off to the bot. 86 | ''' 87 | with self.graph.as_default(): 88 | content = request.json 89 | col = content['i'] 90 | row = content['j'] 91 | print('Received move:') 92 | print((col, row)) 93 | self.bot.apply_move('b', (row, col)) 94 | 95 | bot_row, bot_col = self.bot.select_move('w') 96 | print('Prediction:') 97 | print((bot_col, bot_row)) 98 | result = {'i': bot_col, 'j': bot_row} 99 | json_result = jsonify(**result) 100 | return json_result 101 | 102 | self.app.run(host='0.0.0.0', port=self.port, debug=True, use_reloader=False) 103 | 104 | 105 | class GoModel(object): 106 | '''Tracks a board and selects moves.''' 107 | 108 | def __init__(self, model, processor): 109 | ''' 110 | Parameters: 111 | ----------- 112 | processor: Instance of betago.processor.GoDataLoader, e.g. SevenPlaneProcessor 113 | model: In principle this can be anything that can predict go moves, given data provided by the above 114 | processor. In practice it may very well be (an extension of) a keras model plus glue code. 115 | ''' 116 | self.model = model 117 | self.processor = processor 118 | self.go_board = GoBoard(19) 119 | self.num_planes = processor.num_planes 120 | 121 | def set_board(self, board): 122 | '''Set the board to a specific state.''' 123 | self.go_board = copy.deepcopy(board) 124 | 125 | def apply_move(self, color, move): 126 | ''' Apply the human move''' 127 | return NotImplemented 128 | 129 | def select_move(self, bot_color): 130 | ''' Select a move for the bot''' 131 | return NotImplemented 132 | 133 | 134 | class KerasBot(GoModel): 135 | ''' 136 | KerasBot takes top_n predictions of a keras model and tries to apply the best move. If that move is illegal, 137 | choose the next best, until the list is exhausted. If no more moves are left to play, continue with random 138 | moves until a legal move is found. 139 | ''' 140 | 141 | def __init__(self, model, processor, top_n=10): 142 | super(KerasBot, self).__init__(model=model, processor=processor) 143 | self.top_n = top_n 144 | 145 | def apply_move(self, color, move): 146 | # Apply human move 147 | self.go_board.apply_move(color, move) 148 | 149 | def select_move(self, bot_color): 150 | move = get_first_valid_move(self.go_board, bot_color, 151 | self._move_generator(bot_color)) 152 | if move is not None: 153 | self.go_board.apply_move(bot_color, move) 154 | return move 155 | 156 | def _move_generator(self, bot_color): 157 | return chain( 158 | # First try the model. 159 | self._model_moves(bot_color), 160 | # If none of the model moves are valid, fill in a random 161 | # dame point. This is probably not a very good move, but 162 | # it's better than randomly filling in our own eyes. 163 | fill_dame(self.go_board), 164 | # Lastly just try any open space. 165 | generate_in_random_order(all_empty_points(self.go_board)), 166 | ) 167 | 168 | def _model_moves(self, bot_color): 169 | # Turn the board into a feature vector. 170 | # The (0, 0) is for generating the label, which we ignore. 171 | X, label = self.processor.feature_and_label( 172 | bot_color, (0, 0), self.go_board, self.num_planes) 173 | X = X.reshape((1, X.shape[0], X.shape[1], X.shape[2])) 174 | 175 | # Generate bot move. 176 | pred = np.squeeze(self.model.predict(X)) 177 | top_n_pred_idx = pred.argsort()[-self.top_n:][::-1] 178 | for idx in top_n_pred_idx: 179 | prediction = int(idx) 180 | pred_row = prediction // 19 181 | pred_col = prediction % 19 182 | pred_move = (pred_row, pred_col) 183 | yield pred_move 184 | 185 | 186 | class RandomizedKerasBot(GoModel): 187 | ''' 188 | Takes a weighted sample from the predictions of a keras model. If none of those moves is legal, 189 | pick a random move. 190 | ''' 191 | 192 | def __init__(self, model, processor): 193 | super(RandomizedKerasBot, self).__init__(model=model, processor=processor) 194 | 195 | def apply_move(self, color, move): 196 | # Apply human move 197 | self.go_board.apply_move(color, move) 198 | 199 | def select_move(self, bot_color): 200 | move = get_first_valid_move(self.go_board, bot_color, 201 | self._move_generator(bot_color)) 202 | if move is not None: 203 | self.go_board.apply_move(bot_color, move) 204 | return move 205 | 206 | def _move_generator(self, bot_color): 207 | return chain( 208 | # First try the model. 209 | self._model_moves(bot_color), 210 | # If none of the model moves are valid, fill in a random 211 | # dame point. This is probably not a very good move, but 212 | # it's better than randomly filling in our own eyes. 213 | fill_dame(self.go_board), 214 | # Lastly just try any open space. 215 | generate_in_random_order(all_empty_points(self.go_board)), 216 | ) 217 | 218 | def _model_moves(self, bot_color): 219 | # Turn the board into a feature vector. 220 | # The (0, 0) is for generating the label, which we ignore. 221 | X, label = self.processor.feature_and_label( 222 | bot_color, (0, 0), self.go_board, self.num_planes) 223 | X = X.reshape((1, X.shape[0], X.shape[1], X.shape[2])) 224 | 225 | # Generate moves from the keras model. 226 | n_samples = 20 227 | pred = np.squeeze(self.model.predict(X)) 228 | # Cube the predictions to increase the difference between the 229 | # best and worst moves. Otherwise, it will make too many 230 | # nonsense moves. (There's no scientific basis for this, it's 231 | # just an ad-hoc adjustment) 232 | pred = pred * pred * pred 233 | pred /= pred.sum() 234 | moves = np.random.choice(19 * 19, size=n_samples, replace=False, p=pred) 235 | for prediction in moves: 236 | pred_row = prediction // 19 237 | pred_col = prediction % 19 238 | yield (pred_row, pred_col) 239 | 240 | 241 | class IdiotBot(GoModel): 242 | ''' 243 | Play random moves, like a good 30k bot. 244 | ''' 245 | 246 | def __init__(self, model=None, processor=ThreePlaneProcessor()): 247 | super(IdiotBot, self).__init__(model=model, processor=processor) 248 | 249 | def apply_move(self, color, move): 250 | self.go_board.apply_move(color, move) 251 | 252 | def select_move(self, bot_color): 253 | move = get_first_valid_move( 254 | self.go_board, 255 | bot_color, 256 | # TODO: this function is gone. retrieve it. 257 | generate_randomized(all_empty_points(self.go_board)) 258 | ) 259 | 260 | if move is not None: 261 | self.go_board.apply_move(bot_color, move) 262 | return move 263 | 264 | 265 | def get_first_valid_move(board, color, move_generator): 266 | for move in move_generator: 267 | if move is None or board.is_move_legal(color, move): 268 | return move 269 | return None 270 | 271 | 272 | def generate_in_random_order(point_list): 273 | """Yield all points in the list in a random order.""" 274 | point_list = copy.copy(point_list) 275 | random.shuffle(point_list) 276 | for candidate in point_list: 277 | yield candidate 278 | 279 | 280 | def all_empty_points(board): 281 | """Return all empty positions on the board.""" 282 | empty_points = [] 283 | for point in product(list(range(board.board_size)), list(range(board.board_size))): 284 | if point not in board.board: 285 | empty_points.append(point) 286 | return empty_points 287 | 288 | 289 | def fill_dame(board): 290 | status = scoring.evaluate_territory(board) 291 | # Pass when all dame are filled. 292 | if status.num_dame == 0: 293 | yield None 294 | for dame_point in generate_in_random_order(status.dame_points): 295 | yield dame_point 296 | -------------------------------------------------------------------------------- /betago/networks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/betago/networks/__init__.py -------------------------------------------------------------------------------- /betago/networks/large.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from keras.layers.core import Dense, Activation, Flatten 3 | from keras.layers.convolutional import Conv2D, ZeroPadding2D 4 | 5 | 6 | def layers(input_shape): 7 | return [ 8 | ZeroPadding2D((3, 3), input_shape=input_shape, data_format='channels_first'), 9 | Conv2D(64, (7, 7), padding='valid', data_format='channels_first'), 10 | Activation('relu'), 11 | 12 | ZeroPadding2D((2, 2), data_format='channels_first'), 13 | Conv2D(64, (5, 5), data_format='channels_first'), 14 | Activation('relu'), 15 | 16 | ZeroPadding2D((2, 2), data_format='channels_first'), 17 | Conv2D(64, (5, 5), data_format='channels_first'), 18 | Activation('relu'), 19 | 20 | ZeroPadding2D((2, 2), data_format='channels_first'), 21 | Conv2D(48, (5, 5), data_format='channels_first'), 22 | Activation('relu'), 23 | 24 | ZeroPadding2D((2, 2), data_format='channels_first'), 25 | Conv2D(48, (5, 5), data_format='channels_first'), 26 | Activation('relu'), 27 | 28 | ZeroPadding2D((2, 2), data_format='channels_first'), 29 | Conv2D(32, (5, 5), data_format='channels_first'), 30 | Activation('relu'), 31 | 32 | ZeroPadding2D((2, 2), data_format='channels_first'), 33 | Conv2D(32, (5, 5), data_format='channels_first'), 34 | Activation('relu'), 35 | 36 | Flatten(), 37 | Dense(1024), 38 | Activation('relu'), 39 | ] 40 | -------------------------------------------------------------------------------- /betago/networks/small.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from keras.layers.core import Dense, Activation, Flatten 3 | from keras.layers.convolutional import Conv2D, ZeroPadding2D 4 | 5 | 6 | def layers(input_shape): 7 | return [ 8 | ZeroPadding2D((3, 3), input_shape=input_shape, data_format='channels_first'), 9 | Conv2D(48, (7, 7), padding='valid', data_format='channels_first'), 10 | Activation('relu'), 11 | 12 | ZeroPadding2D((2, 2), data_format='channels_first'), 13 | Conv2D(32, (5, 5), data_format='channels_first'), 14 | Activation('relu'), 15 | 16 | ZeroPadding2D((2, 2), data_format='channels_first'), 17 | Conv2D(32, (5, 5), data_format='channels_first'), 18 | Activation('relu'), 19 | 20 | ZeroPadding2D((2, 2), data_format='channels_first'), 21 | Conv2D(32, (5, 5), data_format='channels_first'), 22 | Activation('relu'), 23 | 24 | Flatten(), 25 | Dense(512), 26 | Activation('relu'), 27 | ] 28 | -------------------------------------------------------------------------------- /betago/processor.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import numpy as np 3 | from .dataloader.base_processor import GoDataProcessor, GoFileProcessor 4 | from six.moves import range 5 | 6 | 7 | class SevenPlaneProcessor(GoDataProcessor): 8 | ''' 9 | Implementation of a Go data processor, using seven planes of 19x19 values to represent the position of 10 | a go board, as explained below. 11 | 12 | This closely reflects the representation suggested in Clark, Storkey: 13 | http://arxiv.org/abs/1412.3409 14 | ''' 15 | 16 | def __init__(self, data_directory='data', num_planes=7, consolidate=True, use_generator=False): 17 | super(SevenPlaneProcessor, self).__init__(data_directory=data_directory, 18 | num_planes=num_planes, 19 | consolidate=consolidate, 20 | use_generator=use_generator) 21 | 22 | def feature_and_label(self, color, move, go_board, num_planes): 23 | ''' 24 | Parameters 25 | ---------- 26 | color: color of the next person to move 27 | move: move they decided to make 28 | go_board: represents the state of the board before they moved 29 | 30 | Planes we write: 31 | 0: our stones with 1 liberty 32 | 1: our stones with 2 liberty 33 | 2: our stones with 3 or more liberties 34 | 3: their stones with 1 liberty 35 | 4: their stones with 2 liberty 36 | 5: their stones with 3 or more liberties 37 | 6: simple ko 38 | ''' 39 | row, col = move 40 | enemy_color = go_board.other_color(color) 41 | label = row * 19 + col 42 | move_array = np.zeros((num_planes, go_board.board_size, go_board.board_size)) 43 | for row in range(0, go_board.board_size): 44 | for col in range(0, go_board.board_size): 45 | pos = (row, col) 46 | if go_board.board.get(pos) == color: 47 | if go_board.go_strings[pos].liberties.size() == 1: 48 | move_array[0, row, col] = 1 49 | elif go_board.go_strings[pos].liberties.size() == 2: 50 | move_array[1, row, col] = 1 51 | elif go_board.go_strings[pos].liberties.size() >= 3: 52 | move_array[2, row, col] = 1 53 | if go_board.board.get(pos) == enemy_color: 54 | if go_board.go_strings[pos].liberties.size() == 1: 55 | move_array[3, row, col] = 1 56 | elif go_board.go_strings[pos].liberties.size() == 2: 57 | move_array[4, row, col] = 1 58 | elif go_board.go_strings[pos].liberties.size() >= 3: 59 | move_array[5, row, col] = 1 60 | if go_board.is_simple_ko(color, pos): 61 | move_array[6, row, col] = 1 62 | return move_array, label 63 | 64 | 65 | class ThreePlaneProcessor(GoDataProcessor): 66 | ''' 67 | Simpler version of the above processor using just three planes. This data processor uses one plane for 68 | stone positions of each color and one for ko. 69 | ''' 70 | 71 | def __init__(self, data_directory='data', num_planes=3, consolidate=True, use_generator=False): 72 | super(ThreePlaneProcessor, self).__init__(data_directory=data_directory, 73 | num_planes=num_planes, 74 | consolidate=consolidate, 75 | use_generator=use_generator) 76 | 77 | def feature_and_label(self, color, move, go_board, num_planes): 78 | ''' 79 | Parameters 80 | ---------- 81 | color: color of the next person to move 82 | move: move they decided to make 83 | go_board: represents the state of the board before they moved 84 | 85 | Planes we write: 86 | 0: our stones 87 | 1: their stones 88 | 2: ko 89 | ''' 90 | row, col = move 91 | enemy_color = go_board.other_color(color) 92 | label = row * 19 + col 93 | move_array = np.zeros((num_planes, go_board.board_size, go_board.board_size)) 94 | for row in range(0, go_board.board_size): 95 | for col in range(0, go_board.board_size): 96 | pos = (row, col) 97 | if go_board.board.get(pos) == color: 98 | move_array[0, row, col] = 1 99 | if go_board.board.get(pos) == enemy_color: 100 | move_array[1, row, col] = 1 101 | if go_board.is_simple_ko(color, pos): 102 | move_array[2, row, col] = 1 103 | return move_array, label 104 | 105 | 106 | class SevenPlaneFileProcessor(GoFileProcessor): 107 | ''' 108 | File processor corresponding to the above data processor. Loading all available data into memory is simply 109 | not feasible, and this class allows preprocessing into an efficient, binary format. 110 | ''' 111 | def __init__(self, data_directory='data', num_planes=7, consolidate=True): 112 | super(SevenPlaneFileProcessor, self).__init__(data_directory=data_directory, 113 | num_planes=num_planes, consolidate=consolidate) 114 | 115 | def store_results(self, data_file, color, move, go_board): 116 | ''' 117 | Parameters 118 | ---------- 119 | color: color of the next person to move 120 | move: move they decided to make 121 | go_board: represents the state of the board before they moved 122 | 123 | Planes we write: 124 | 0: our stones with 1 liberty 125 | 1: our stones with 2 liberty 126 | 2: our stones with 3 or more liberties 127 | 3: their stones with 1 liberty 128 | 4: their stones with 2 liberty 129 | 5: their stones with 3 or more liberties 130 | 6: simple ko 131 | ''' 132 | row, col = move 133 | enemy_color = go_board.other_color(color) 134 | data_file.write('GO') 135 | label = row * 19 + col 136 | data_file.write(chr(label % 256)) 137 | data_file.write(chr(label // 256)) 138 | data_file.write(chr(0)) 139 | data_file.write(chr(0)) 140 | thisbyte = 0 141 | thisbitpos = 0 142 | for plane in range(0, 7): 143 | for row in range(0, go_board.board_size): 144 | for col in range(0, go_board.board_size): 145 | thisbit = 0 146 | pos = (row, col) 147 | if go_board.board.get(pos) == color: 148 | if plane == 0 and go_board.go_strings[pos].liberties.size() == 1: 149 | thisbit = 1 150 | elif plane == 1 and go_board.go_strings[pos].liberties.size() == 2: 151 | thisbit = 1 152 | elif plane == 2 and go_board.go_strings[pos].liberties.size() >= 3: 153 | thisbit = 1 154 | if go_board.board.get(pos) == enemy_color: 155 | if plane == 3 and go_board.go_strings[pos].liberties.size() == 1: 156 | thisbit = 1 157 | elif plane == 4 and go_board.go_strings[pos].liberties.size() == 2: 158 | thisbit = 1 159 | elif plane == 5 and go_board.go_strings[pos].liberties.size() >= 3: 160 | thisbit = 1 161 | if plane == 6 and go_board.is_simple_ko(color, pos): 162 | thisbit = 1 163 | thisbyte = thisbyte + (thisbit << (7 - thisbitpos)) 164 | thisbitpos = thisbitpos + 1 165 | if thisbitpos == 8: 166 | data_file.write(chr(thisbyte)) 167 | thisbitpos = 0 168 | thisbyte = 0 169 | if thisbitpos != 0: 170 | data_file.write(chr(thisbyte)) 171 | -------------------------------------------------------------------------------- /betago/scoring.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import itertools 3 | from six.moves import range 4 | 5 | 6 | class Territory(object): 7 | def __init__(self, territory_map): 8 | self.num_black_territory = 0 9 | self.num_white_territory = 0 10 | self.num_black_stones = 0 11 | self.num_white_stones = 0 12 | self.num_dame = 0 13 | self.dame_points = [] 14 | for point, status in territory_map.items(): 15 | if status == 'b': 16 | self.num_black_stones += 1 17 | elif status == 'w': 18 | self.num_white_stones += 1 19 | elif status == 'territory_b': 20 | self.num_black_territory += 1 21 | elif status == 'territory_w': 22 | self.num_white_territory += 1 23 | elif status == 'dame': 24 | self.num_dame += 1 25 | self.dame_points.append(point) 26 | 27 | 28 | def evaluate_territory(board): 29 | """Map a board into territory and dame. 30 | 31 | Any points that are completely surrounded by a single color are 32 | counted as territory; it makes no attempt to identify even 33 | trivially dead groups. 34 | """ 35 | status = {} 36 | for r, c in itertools.product(list(range(board.board_size)), list(range(board.board_size))): 37 | if (r, c) in status: 38 | # Already visited this as part of a different group. 39 | continue 40 | if (r, c) in board.board: 41 | # It's a stone. 42 | status[r, c] = board.board[r, c] 43 | else: 44 | group, neighbors = _collect_region((r, c), board) 45 | if len(neighbors) == 1: 46 | # Completely surrounded by black or white. 47 | fill_with = 'territory_' + neighbors.pop() 48 | else: 49 | # Dame. 50 | fill_with = 'dame' 51 | for pos in group: 52 | status[pos] = fill_with 53 | return Territory(status) 54 | 55 | 56 | def _collect_region(start_pos, board, visited=None): 57 | """Find the contiguous section of a board containing a point. Also 58 | identify all the boundary points. 59 | """ 60 | if visited is None: 61 | visited = {} 62 | if start_pos in visited: 63 | return [], set() 64 | all_points = [start_pos] 65 | all_borders = set() 66 | visited[start_pos] = True 67 | here = board.board.get(start_pos) 68 | r, c = start_pos 69 | deltas = [(-1, 0), (1, 0), (0, -1), (0, 1)] 70 | for delta_r, delta_c in deltas: 71 | next_r, next_c = r + delta_r, c + delta_c 72 | if next_r < 0 or next_r >= board.board_size: 73 | continue 74 | if next_c < 0 or next_c >= board.board_size: 75 | continue 76 | neighbor = board.board.get((next_r, next_c)) 77 | if neighbor == here: 78 | points, borders = _collect_region((next_r, next_c), board, visited) 79 | all_points += points 80 | all_borders |= borders 81 | else: 82 | all_borders.add(neighbor) 83 | return all_points, all_borders 84 | -------------------------------------------------------------------------------- /betago/simulate.py: -------------------------------------------------------------------------------- 1 | def simulate_game(board, black_bot, white_bot): 2 | """Simulate a game between two bots.""" 3 | move_num = 1 4 | whose_turn = 'b' 5 | moves = [] 6 | while moves[-2:] != [None, None]: 7 | if whose_turn == 'b': 8 | next_move = black_bot.select_move('b') 9 | if next_move is not None: 10 | white_bot.apply_move('b', next_move) 11 | else: 12 | next_move = white_bot.select_move('w') 13 | if next_move is not None: 14 | black_bot.apply_move('w', next_move) 15 | if next_move is not None: 16 | board.apply_move(whose_turn, next_move) 17 | moves.append(next_move) 18 | move_num += 1 19 | whose_turn = 'b' if whose_turn == 'w' else 'w' 20 | -------------------------------------------------------------------------------- /betago/training/__init__.py: -------------------------------------------------------------------------------- 1 | from .checkpoint import * 2 | -------------------------------------------------------------------------------- /betago/training/checkpoint.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import os 3 | 4 | import h5py 5 | from keras.models import Sequential 6 | from keras.optimizers import Adadelta 7 | from keras.layers.core import Dense, Activation 8 | 9 | from . import kerashack 10 | 11 | 12 | __all__ = [ 13 | 'TrainingRun', 14 | ] 15 | 16 | 17 | class TrainingRun(object): 18 | 19 | def __init__(self, filename, model, epochs_completed, chunks_completed, num_chunks): 20 | self.filename = filename 21 | self.model = model 22 | self.epochs_completed = epochs_completed 23 | self.chunks_completed = chunks_completed 24 | self.num_chunks = num_chunks 25 | 26 | def save(self): 27 | # Backup the original file in case something goes wrong while 28 | # saving the new checkpoint. 29 | backup = None 30 | if os.path.exists(self.filename): 31 | backup = self.filename + '.bak' 32 | os.rename(self.filename, backup) 33 | 34 | output = h5py.File(self.filename, 'w') 35 | model_out = output.create_group('model') 36 | kerashack.save_model_to_hdf5_group(self.model, model_out) 37 | metadata = output.create_group('metadata') 38 | metadata.attrs['epochs_completed'] = self.epochs_completed 39 | metadata.attrs['chunks_completed'] = self.chunks_completed 40 | metadata.attrs['num_chunks'] = self.num_chunks 41 | output.close() 42 | 43 | # If we got here, we no longer need the backup. 44 | if backup is not None: 45 | os.unlink(backup) 46 | 47 | def complete_chunk(self): 48 | self.chunks_completed += 1 49 | if self.chunks_completed == self.num_chunks: 50 | self.epochs_completed += 1 51 | self.chunks_completed = 0 52 | self.save() 53 | 54 | @classmethod 55 | def load(cls, filename): 56 | inp = h5py.File(filename, 'r') 57 | model = kerashack.load_model_from_hdf5_group(inp['model']) 58 | training_run = cls(filename, 59 | model, 60 | inp['metadata'].attrs['epochs_completed'], 61 | inp['metadata'].attrs['chunks_completed'], 62 | inp['metadata'].attrs['num_chunks']) 63 | inp.close() 64 | return training_run 65 | 66 | @classmethod 67 | def create(cls, filename, index, layer_fn): 68 | model = Sequential() 69 | for layer in layer_fn((7, 19, 19)): 70 | model.add(layer) 71 | model.add(Dense(19 * 19)) 72 | model.add(Activation('softmax')) 73 | opt = Adadelta(clipnorm=0.25) 74 | model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy']) 75 | training_run = cls(filename, model, 0, 0, index.num_chunks) 76 | training_run.save() 77 | return training_run 78 | -------------------------------------------------------------------------------- /betago/training/kerashack.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import tempfile 3 | import os 4 | 5 | import h5py 6 | from keras.models import load_model, save_model 7 | 8 | 9 | def save_model_to_hdf5_group(model, f): 10 | # Use Keras save_model to save the full model (including optimizer 11 | # state) to a file. 12 | # Then we can embed the contents of that HDF5 file inside ours. 13 | tempfd, tempfname = tempfile.mkstemp(prefix='tmp-betago') 14 | try: 15 | os.close(tempfd) 16 | save_model(model, tempfname) 17 | serialized_model = h5py.File(tempfname, 'r') 18 | root_item = serialized_model.get('/') 19 | serialized_model.copy(root_item, f, 'kerasmodel') 20 | serialized_model.close() 21 | finally: 22 | os.unlink(tempfname) 23 | 24 | 25 | def load_model_from_hdf5_group(f, custom_objects=None): 26 | # Extract the model into a temporary file. Then we can use Keras 27 | # load_model to read it. 28 | tempfd, tempfname = tempfile.mkstemp(prefix='tmp-betago') 29 | try: 30 | os.close(tempfd) 31 | serialized_model = h5py.File(tempfname, 'w') 32 | root_item = f.get('kerasmodel') 33 | for attr_name, attr_value in root_item.attrs.items(): 34 | serialized_model.attrs[attr_name] = attr_value 35 | for k in root_item.keys(): 36 | f.copy(root_item.get(k), serialized_model, k) 37 | serialized_model.close() 38 | return load_model(tempfname) 39 | finally: 40 | os.unlink(tempfname) 41 | -------------------------------------------------------------------------------- /betagopy2-environmnet.yml: -------------------------------------------------------------------------------- 1 | name: betagopy2 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - backports=1.0=py27_1 7 | - backports.weakref=1.0rc1=py27_1 8 | - bleach=1.5.0=py27_0 9 | - ca-certificates=2017.7.27.1=0 10 | - certifi=2017.7.27.1=py27_0 11 | - funcsigs=1.0.2=py_2 12 | - html5lib=0.9999999=py27_0 13 | - markdown=2.6.9=py27_0 14 | - mock=2.0.0=py27_0 15 | - ncurses=5.9=10 16 | - openssl=1.0.2l=0 17 | - pbr=3.1.1=py27_0 18 | - pip=9.0.1=py27_0 19 | - protobuf=3.4.0=py27_0 20 | - python=2.7.14=0 21 | - readline=6.2=0 22 | - setuptools=36.6.0=py27_1 23 | - six=1.11.0=py27_1 24 | - sqlite=3.13.0=1 25 | - tensorflow=1.3.0=py27_0 26 | - tk=8.5.19=2 27 | - webencodings=0.5=py27_0 28 | - werkzeug=0.12.2=py_1 29 | - wheel=0.30.0=py_1 30 | - zlib=1.2.8=3 31 | - mkl=2017.0.3=0 32 | - numpy=1.13.1=py27_0 33 | - pip: 34 | - betago==0.2 35 | - click==6.7 36 | - flask==0.12.2 37 | - flask-cors==3.0.3 38 | - future==0.16.0 39 | - gomill==0.8 40 | - h5py==2.7.1 41 | - itsdangerous==0.24 42 | - jinja2==2.9.6 43 | - keras==2.0.8 44 | - markupsafe==1.0 45 | - pyyaml==3.12 46 | - scipy==1.0.0 47 | prefix: /home/arushi/anaconda3/envs/betagopy2 48 | 49 | -------------------------------------------------------------------------------- /ear_reddening.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ear_reddening.png -------------------------------------------------------------------------------- /examples/bot_vs_bot.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import yaml 3 | 4 | from keras.models import model_from_yaml 5 | from betago import scoring 6 | from betago.dataloader import goboard 7 | from betago.model import KerasBot 8 | from betago.processor import SevenPlaneProcessor 9 | from betago.simulate import simulate_game 10 | 11 | 12 | def load_keras_bot(bot_name): 13 | model_file = 'model_zoo/' + bot_name + '_bot.yml' 14 | weight_file = 'model_zoo/' + bot_name + '_weights.hd5' 15 | with open(model_file, 'r') as f: 16 | yml = yaml.load(f) 17 | model = model_from_yaml(yaml.dump(yml)) 18 | # Note that in Keras 1.0 we have to recompile the model explicitly 19 | model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy']) 20 | model.load_weights(weight_file) 21 | processor = SevenPlaneProcessor() 22 | return KerasBot(model=model, processor=processor) 23 | 24 | 25 | def main(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument('black_bot_name') 28 | parser.add_argument('white_bot_name') 29 | parser.add_argument('--komi', '-k', type=float, default=5.5) 30 | args = parser.parse_args() 31 | 32 | black_bot = load_keras_bot(args.black_bot_name) 33 | white_bot = load_keras_bot(args.white_bot_name) 34 | 35 | print("Simulating %s vs %s..." % (args.black_bot_name, args.white_bot_name)) 36 | board = goboard.GoBoard() 37 | simulate_game(board, black_bot=black_bot, white_bot=white_bot) 38 | print("Game over!") 39 | print(goboard.to_string(board)) 40 | # Does not remove dead stones. 41 | print("\nScore (Chinese rules):") 42 | status = scoring.evaluate_territory(board) 43 | black_area = status.num_black_territory + status.num_black_stones 44 | white_area = status.num_white_territory + status.num_white_stones 45 | white_score = white_area + args.komi 46 | print("Black %d" % black_area) 47 | print("White %d + %.1f = %.1f" % (white_area, args.komi, white_score)) 48 | 49 | 50 | if __name__ == '__main__': 51 | main() 52 | -------------------------------------------------------------------------------- /examples/end_to_end.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | import webbrowser 4 | from keras.models import Sequential 5 | from keras.layers.core import Dense, Dropout, Activation, Flatten 6 | from keras.layers.convolutional import Convolution2D, MaxPooling2D 7 | from keras.utils import np_utils 8 | from betago.model import KerasBot 9 | from betago.processor import SevenPlaneProcessor 10 | 11 | batch_size = 128 12 | nb_epoch = 20 13 | 14 | nb_classes = 19 * 19 # One class for each position on the board 15 | go_board_rows, go_board_cols = 19, 19 # input dimensions of go board 16 | nb_filters = 32 # number of convolutional filters to use 17 | nb_pool = 2 # size of pooling area for max pooling 18 | nb_conv = 3 # convolution kernel size 19 | 20 | # SevenPlaneProcessor loads seven planes (doh!) of 19*19 data points, so we need 7 input channels 21 | processor = SevenPlaneProcessor() 22 | input_channels = processor.num_planes 23 | 24 | # Load go data from 1000 KGS games and one-hot encode labels 25 | X, y = processor.load_go_data(num_samples=1000) 26 | X = X.astype('float32') 27 | Y = np_utils.to_categorical(y, nb_classes) 28 | 29 | # Specify a keras model with two convolutional layers and two dense layers, 30 | # connecting the (num_samples, 7, 19, 19) input to the 19*19 output vector. 31 | model = Sequential() 32 | model.add(Convolution2D(nb_filters, nb_conv, nb_conv, border_mode='valid', 33 | input_shape=(input_channels, go_board_rows, go_board_cols))) 34 | model.add(Activation('relu')) 35 | model.add(Convolution2D(nb_filters, nb_conv, nb_conv)) 36 | model.add(Activation('relu')) 37 | model.add(MaxPooling2D(pool_size=(nb_pool, nb_pool))) 38 | model.add(Dropout(0.2)) 39 | model.add(Flatten()) 40 | model.add(Dense(256)) 41 | model.add(Activation('relu')) 42 | model.add(Dropout(0.5)) 43 | model.add(Dense(nb_classes)) 44 | model.add(Activation('softmax')) 45 | model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy']) 46 | 47 | # Fit model to data 48 | model.fit(X, Y, batch_size=batch_size, nb_epoch=nb_epoch, verbose=1) 49 | 50 | # Open web frontend 51 | path = os.getcwd().replace('/examples', '') 52 | webbrowser.open('file://' + path + '/ui/demoBot.html', new=2) 53 | 54 | # Create a bot from processor and model, then serve it. 55 | go_model = KerasBot(model=model, processor=processor) 56 | go_model.run() 57 | -------------------------------------------------------------------------------- /examples/play_gnugo.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import yaml 3 | import subprocess 4 | import re 5 | import argparse 6 | 7 | from keras.models import model_from_yaml 8 | from betago.model import KerasBot 9 | from betago.processor import SevenPlaneProcessor 10 | from betago.gtp.board import gtp_position_to_coords, coords_to_gtp_position 11 | 12 | argparser = argparse.ArgumentParser() 13 | argparser.add_argument('handicap', type=int, nargs=1) 14 | argparser.add_argument('output_sgf', nargs='?', default='output.sgf') 15 | args = argparser.parse_args() 16 | 17 | processor = SevenPlaneProcessor() 18 | 19 | bot_name = '100_epochs_cnn' 20 | model_file = 'model_zoo/' + bot_name + '_bot.yml' 21 | weight_file = 'model_zoo/' + bot_name + '_weights.hd5' 22 | 23 | with open(model_file, 'r') as f: 24 | yml = yaml.load(f) 25 | model = model_from_yaml(yaml.dump(yml)) 26 | # Note that in Keras 1.0 we have to recompile the model explicitly 27 | model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy']) 28 | model.load_weights(weight_file) 29 | 30 | bot = KerasBot(model=model, processor=processor) 31 | 32 | gnugo_cmd = ["gnugo", "--mode", "gtp"] 33 | p = subprocess.Popen(gnugo_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 34 | 35 | 36 | def send_command(gtpStream, cmd): 37 | gtpStream.stdin.write(cmd) 38 | print(cmd.strip()) 39 | 40 | 41 | def get_response(gtpStream): 42 | succeeded = False 43 | result = '' 44 | while succeeded == False: 45 | line = gtpStream.stdout.readline() 46 | if line[0] == '=': 47 | succeeded = True 48 | line = line.strip() 49 | print("Response is: " + line) 50 | result = re.sub('^= ?', '', line) 51 | return result 52 | 53 | letters = 'abcdefghijklmnopqrs' 54 | 55 | 56 | def sgfCoord(coords): 57 | row, col = coords 58 | return letters[col] + letters[18 - row] 59 | 60 | # deal with handicap. Parse multi-stone response to see where it was placed. 61 | handicap = args.handicap[0] 62 | 63 | send_command(p, "boardsize 19\n") 64 | get_response(p) 65 | 66 | sgf = "(;GM[1]FF[4]CA[UTF-8]SZ[19]RU[Chinese]\n" 67 | 68 | if(handicap == 0): 69 | send_command(p, "komi 7.5\n") 70 | get_response(p) 71 | sgf = sgf + "KM[7.5]\n" 72 | else: 73 | send_command(p, "fixed_handicap " + str(handicap) + "\n") 74 | stones = get_response(p) 75 | sgf_handicap = "HA[" + str(handicap) + "]AB" 76 | for pos in stones.split(" "): 77 | move = gtp_position_to_coords(pos) 78 | bot.apply_move('b', move) 79 | sgf_handicap = sgf_handicap + "[" + sgfCoord(move) + "]" 80 | sgf = sgf + sgf_handicap + "\n" 81 | 82 | passes = 0 83 | our_color = 'b' # assume we are black for now 84 | their_color = 'w' # assume we are black for now 85 | last_color = 'w' 86 | 87 | if(handicap > 1): 88 | last_color = 'b' 89 | 90 | colors = {} 91 | colors['w'] = 'white' 92 | colors['b'] = 'black' 93 | 94 | while passes < 2: 95 | if(last_color != our_color): 96 | move = bot.select_move(our_color) # applies the move too 97 | if move is None: 98 | send_command(p, "play " + colors[our_color] + " pass\n") 99 | sgf = sgf + ";" + our_color.upper() + "[]\n" 100 | passes = passes + 1 101 | else: 102 | pos = coords_to_gtp_position(move) 103 | send_command(p, "play " + colors[our_color] + " " + pos + "\n") 104 | sgf = sgf + ";" + our_color.upper() + "[" + sgfCoord(move) + "]\n" 105 | passes = 0 106 | resp = get_response(p) 107 | last_color = our_color 108 | else: 109 | send_command(p, "genmove " + colors[their_color] + "\n") 110 | pos = get_response(p) 111 | if(pos == 'RESIGN'): 112 | passes = 2 113 | elif(pos == 'PASS'): 114 | sgf = sgf + ";" + their_color.upper() + "[]\n" 115 | passes = passes + 1 116 | else: 117 | move = gtp_position_to_coords(pos) 118 | bot.apply_move(their_color, move) 119 | sgf = sgf + ";" + their_color.upper() + "[" + sgfCoord(move) + "]\n" 120 | passes = 0 121 | last_color = their_color 122 | 123 | sgf = sgf + ")\n" 124 | 125 | with open(args.output_sgf, 'w') as out_h: 126 | out_h.write(sgf) 127 | -------------------------------------------------------------------------------- /examples/play_pachi.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import yaml 3 | import subprocess 4 | import re 5 | import argparse 6 | 7 | from keras.models import model_from_yaml 8 | from betago.model import KerasBot 9 | from betago.processor import SevenPlaneProcessor 10 | from betago.gtp.board import gtp_position_to_coords, coords_to_gtp_position 11 | 12 | argparser = argparse.ArgumentParser() 13 | argparser.add_argument('handicap', type=int, nargs=1) 14 | argparser.add_argument('output_sgf', nargs='?', default='output.sgf') 15 | args = argparser.parse_args() 16 | 17 | processor = SevenPlaneProcessor() 18 | 19 | bot_name = '100_epochs_cnn' 20 | model_file = 'model_zoo/' + bot_name + '_bot.yml' 21 | weight_file = 'model_zoo/' + bot_name + '_weights.hd5' 22 | 23 | with open(model_file, 'r') as f: 24 | yml = yaml.load(f) 25 | model = model_from_yaml(yaml.dump(yml)) 26 | # Note that in Keras 1.0 we have to recompile the model explicitly 27 | model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy']) 28 | model.load_weights(weight_file) 29 | 30 | bot = KerasBot(model=model, processor=processor) 31 | 32 | pachi_cmd = ["pachi"] 33 | p = subprocess.Popen(pachi_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 34 | 35 | 36 | def send_command(gtpStream, cmd): 37 | gtpStream.stdin.write(cmd) 38 | print(cmd.strip()) 39 | 40 | 41 | def get_response(gtpStream): 42 | succeeded = False 43 | result = '' 44 | while succeeded == False: 45 | line = gtpStream.stdout.readline() 46 | if line[0] == '=': 47 | succeeded = True 48 | line = line.strip() 49 | print("Response is: " + line) 50 | result = re.sub('^= ?', '', line) 51 | return result 52 | 53 | letters = 'abcdefghijklmnopqrs' 54 | 55 | 56 | def sgfCoord(coords): 57 | row, col = coords 58 | return letters[col] + letters[18 - row] 59 | 60 | # deal with handicap. Parse multi-stone response to see where it was placed. 61 | handicap = args.handicap[0] 62 | 63 | send_command(p, "boardsize 19\n") 64 | get_response(p) 65 | 66 | sgf = "(;GM[1]FF[4]CA[UTF-8]SZ[19]RU[Chinese]\n" 67 | 68 | if(handicap == 0): 69 | send_command(p, "komi 7.5\n") 70 | get_response(p) 71 | sgf = sgf + "KM[7.5]\n" 72 | else: 73 | send_command(p, "fixed_handicap " + str(handicap) + "\n") 74 | stones = get_response(p) 75 | sgf_handicap = "HA[" + str(handicap) + "]AB" 76 | for pos in stones.split(" "): 77 | move = gtp_position_to_coords(pos) 78 | bot.apply_move('b', move) 79 | sgf_handicap = sgf_handicap + "[" + sgfCoord(move) + "]" 80 | sgf = sgf + sgf_handicap + "\n" 81 | 82 | passes = 0 83 | our_color = 'b' # assume we are black for now 84 | their_color = 'w' # assume we are black for now 85 | last_color = 'w' 86 | 87 | if(handicap > 1): 88 | last_color = 'b' 89 | 90 | colors = {} 91 | colors['w'] = 'white' 92 | colors['b'] = 'black' 93 | 94 | while passes < 2: 95 | if(last_color != our_color): 96 | move = bot.select_move(our_color) # applies the move too 97 | if move is None: 98 | send_command(p, "play " + colors[our_color] + " pass\n") 99 | sgf = sgf + ";" + our_color.upper() + "[]\n" 100 | passes = passes + 1 101 | else: 102 | pos = coords_to_gtp_position(move) 103 | send_command(p, "play " + colors[our_color] + " " + pos + "\n") 104 | sgf = sgf + ";" + our_color.upper() + "[" + sgfCoord(move) + "]\n" 105 | passes = 0 106 | resp = get_response(p) 107 | last_color = our_color 108 | else: 109 | send_command(p, "genmove " + colors[their_color] + "\n") 110 | pos = get_response(p) 111 | if(pos == 'resign'): 112 | passes = 2 113 | elif(pos == 'pass'): 114 | sgf = sgf + ";" + their_color.upper() + "[]\n" 115 | passes = passes + 1 116 | else: 117 | move = gtp_position_to_coords(pos) 118 | bot.apply_move(their_color, move) 119 | sgf = sgf + ";" + their_color.upper() + "[" + sgfCoord(move) + "]\n" 120 | passes = 0 121 | last_color = their_color 122 | 123 | sgf = sgf + ")\n" 124 | 125 | with open(args.output_sgf, 'w') as out_h: 126 | out_h.write(sgf) 127 | -------------------------------------------------------------------------------- /examples/run_gtp.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import yaml 3 | 4 | from keras.models import model_from_yaml 5 | from betago.model import KerasBot 6 | from betago.gtp import GTPFrontend 7 | from betago.processor import SevenPlaneProcessor 8 | 9 | processor = SevenPlaneProcessor() 10 | 11 | bot_name = 'demo' 12 | model_file = 'model_zoo/' + bot_name + '_bot.yml' 13 | weight_file = 'model_zoo/' + bot_name + '_weights.hd5' 14 | 15 | 16 | with open(model_file, 'r') as f: 17 | yml = yaml.load(f) 18 | model = model_from_yaml(yaml.dump(yml)) 19 | # Note that in Keras 1.0 we have to recompile the model explicitly 20 | model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy']) 21 | model.load_weights(weight_file) 22 | 23 | # Start GTP frontend and run model. 24 | frontend = GTPFrontend(bot=KerasBot(model=model, processor=processor)) 25 | frontend.run() 26 | -------------------------------------------------------------------------------- /examples/run_idiot_bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import webbrowser 3 | from betago.model import IdiotBot 4 | 5 | # Open web frontend 6 | path = os.getcwd().replace('/examples', '') 7 | webbrowser.open('file://' + path + '/ui/demoBot.html', new=2) 8 | 9 | # Create an idiot bot, creating random moves and serve it. 10 | go_bot = IdiotBot() 11 | go_bot.run() 12 | -------------------------------------------------------------------------------- /examples/train_and_store.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from keras.models import Sequential 3 | from keras.layers.core import Dense, Dropout, Activation, Flatten 4 | from keras.layers.convolutional import Conv2D, MaxPooling2D 5 | from keras.utils import np_utils 6 | 7 | from betago.processor import SevenPlaneProcessor 8 | 9 | batch_size = 128 10 | nb_epoch = 100 11 | 12 | nb_classes = 19 * 19 # One class for each position on the board 13 | go_board_rows, go_board_cols = 19, 19 # input dimensions of go board 14 | nb_filters = 32 # number of convolutional filters to use 15 | nb_pool = 2 # size of pooling area for max pooling 16 | nb_conv = 3 # convolution kernel size 17 | 18 | # SevenPlaneProcessor loads seven planes (doh!) of 19*19 data points, so we need 7 input channels 19 | processor = SevenPlaneProcessor() 20 | input_channels = processor.num_planes 21 | 22 | # Load go data and one-hot encode labels 23 | X, y = processor.load_go_data(num_samples=1000) 24 | X = X.astype('float32') 25 | Y = np_utils.to_categorical(y, nb_classes) 26 | 27 | # Specify a keras model with two convolutional layers and two dense layers, 28 | # connecting the (num_samples, 7, 19, 19) input to the 19*19 output vector. 29 | model = Sequential() 30 | model.add(Conv2D(nb_filters, (nb_conv, nb_conv), padding='valid', 31 | input_shape=(input_channels, go_board_rows, go_board_cols), 32 | data_format='channels_first')) 33 | model.add(Activation('relu')) 34 | model.add(Conv2D(nb_filters, (nb_conv, nb_conv), data_format='channels_first')) 35 | model.add(Activation('relu')) 36 | model.add(MaxPooling2D(pool_size=(nb_pool, nb_pool))) 37 | model.add(Dropout(0.25)) 38 | model.add(Flatten()) 39 | model.add(Dense(256)) 40 | model.add(Activation('relu')) 41 | model.add(Dropout(0.5)) 42 | model.add(Dense(nb_classes)) 43 | model.add(Activation('softmax')) 44 | model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy']) 45 | 46 | model.fit(X, Y, batch_size=batch_size, epochs=nb_epoch, verbose=1) 47 | 48 | weight_file = '../model_zoo/weights.hd5' 49 | model.save_weights(weight_file, overwrite=True) 50 | 51 | model_file = '../model_zoo/model.yml' 52 | with open(model_file, 'w') as yml: 53 | model_yaml = model.to_yaml() 54 | yml.write(model_yaml) 55 | -------------------------------------------------------------------------------- /examples/train_generator.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import argparse 4 | import os 5 | 6 | from keras.callbacks import ModelCheckpoint 7 | from keras.models import Sequential 8 | from keras.layers.core import Dense, Dropout, Activation, Flatten 9 | from keras.layers.convolutional import Conv2D, MaxPooling2D 10 | 11 | from betago.processor import SevenPlaneProcessor 12 | 13 | 14 | parser = argparse.ArgumentParser() 15 | parser.add_argument('--bot-name', default='new_bot') 16 | parser.add_argument('--epochs', type=int, default=5) 17 | parser.add_argument('--sample-size', type=int, default=1000) 18 | args = parser.parse_args() 19 | 20 | here = os.path.dirname(os.path.abspath(__file__)) 21 | model_zoo = os.path.join(here, '..', 'model_zoo') 22 | weight_file = os.path.join(model_zoo, args.bot_name + '_weights.hd5') 23 | checkpoint_file_pattern = os.path.join(model_zoo, args.bot_name + '_epoch_{epoch}.hd5') 24 | model_file = os.path.join(model_zoo, args.bot_name + '_model.yml') 25 | 26 | batch_size = 128 27 | 28 | nb_classes = 19 * 19 # One class for each position on the board 29 | go_board_rows, go_board_cols = 19, 19 # input dimensions of go board 30 | nb_filters = 32 # number of convolutional filters to use 31 | nb_pool = 2 # size of pooling area for max pooling 32 | nb_conv = 3 # convolution kernel size 33 | 34 | # SevenPlaneProcessor loads seven planes (doh!) of 19*19 data points, so we need 7 input channels 35 | processor = SevenPlaneProcessor(use_generator=True) 36 | input_channels = processor.num_planes 37 | 38 | # Load go data and one-hot encode labels 39 | data_generator = processor.load_go_data(num_samples=args.sample_size) 40 | print(data_generator.get_num_samples()) 41 | 42 | # Specify a keras model with two convolutional layers and two dense layers, 43 | # connecting the (num_samples, 7, 19, 19) input to the 19*19 output vector. 44 | model = Sequential() 45 | model.add(Conv2D(nb_filters, (nb_conv, nb_conv), padding='valid', 46 | input_shape=(input_channels, go_board_rows, go_board_cols), 47 | data_format='channels_first')) 48 | model.add(Activation('relu')) 49 | model.add(Conv2D(nb_filters, (nb_conv, nb_conv), data_format='channels_first')) 50 | model.add(Activation('relu')) 51 | model.add(MaxPooling2D(pool_size=(nb_pool, nb_pool))) 52 | model.add(Dropout(0.25)) 53 | model.add(Flatten()) 54 | model.add(Dense(256)) 55 | model.add(Activation('relu')) 56 | model.add(Dropout(0.5)) 57 | model.add(Dense(nb_classes)) 58 | model.add(Activation('softmax')) 59 | model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy']) 60 | 61 | model.fit_generator(data_generator.generate(batch_size=batch_size, nb_classes=nb_classes), 62 | samples_per_epoch=data_generator.get_num_samples(), nb_epoch=args.epochs, 63 | callbacks=[ModelCheckpoint(checkpoint_file_pattern)]) 64 | 65 | model.save_weights(weight_file, overwrite=True) 66 | with open(model_file, 'w') as yml: 67 | model_yaml = model.to_yaml() 68 | yml.write(model_yaml) 69 | -------------------------------------------------------------------------------- /model_zoo/100_epochs_cnn_bot.yml: -------------------------------------------------------------------------------- 1 | class_name: Sequential 2 | config: 3 | - class_name: Convolution2D 4 | config: 5 | W_constraint: null 6 | W_regularizer: null 7 | activation: linear 8 | activity_regularizer: null 9 | b_constraint: null 10 | b_regularizer: null 11 | batch_input_shape: !!python/tuple [null, 7, 19, 19] 12 | border_mode: valid 13 | dim_ordering: th 14 | init: glorot_uniform 15 | input_dtype: float32 16 | name: convolution2d_1 17 | nb_col: 3 18 | nb_filter: 32 19 | nb_row: 3 20 | subsample: &id001 !!python/tuple [1, 1] 21 | trainable: true 22 | - class_name: Activation 23 | config: {activation: relu, name: activation_1, trainable: true} 24 | - class_name: Convolution2D 25 | config: 26 | W_constraint: null 27 | W_regularizer: null 28 | activation: linear 29 | activity_regularizer: null 30 | b_constraint: null 31 | b_regularizer: null 32 | border_mode: valid 33 | dim_ordering: th 34 | init: glorot_uniform 35 | name: convolution2d_2 36 | nb_col: 3 37 | nb_filter: 32 38 | nb_row: 3 39 | subsample: *id001 40 | trainable: true 41 | - class_name: Activation 42 | config: {activation: relu, name: activation_2, trainable: true} 43 | - class_name: MaxPooling2D 44 | config: 45 | border_mode: valid 46 | dim_ordering: th 47 | name: maxpooling2d_1 48 | pool_size: &id002 !!python/tuple [2, 2] 49 | strides: *id002 50 | trainable: true 51 | - class_name: Dropout 52 | config: {name: dropout_1, p: 0.25, trainable: true} 53 | - class_name: Flatten 54 | config: {name: flatten_1, trainable: true} 55 | - class_name: Dense 56 | config: {W_constraint: null, W_regularizer: null, activation: linear, activity_regularizer: null, 57 | b_constraint: null, b_regularizer: null, init: glorot_uniform, input_dim: null, 58 | name: dense_1, output_dim: 256, trainable: true} 59 | - class_name: Activation 60 | config: {activation: relu, name: activation_3, trainable: true} 61 | - class_name: Dropout 62 | config: {name: dropout_2, p: 0.5, trainable: true} 63 | - class_name: Dense 64 | config: {W_constraint: null, W_regularizer: null, activation: linear, activity_regularizer: null, 65 | b_constraint: null, b_regularizer: null, init: glorot_uniform, input_dim: null, 66 | name: dense_2, output_dim: 361, trainable: true} 67 | - class_name: Activation 68 | config: {activation: softmax, name: activation_4, trainable: true} 69 | -------------------------------------------------------------------------------- /model_zoo/100_epochs_cnn_weights.hd5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/model_zoo/100_epochs_cnn_weights.hd5 -------------------------------------------------------------------------------- /model_zoo/README.md: -------------------------------------------------------------------------------- 1 | # BetaGo Model zoo 2 | 3 | Currently there is just two relatively naive bots available, both are trained with ```SevenPlaneProcessor``` on 1000 games with Theano backend. One model is trained with 10 epochs, the other with just one. Test both to see the difference in performance. The better model gets about 8% of the moves from training data right, the other one only about 1%. 4 | -------------------------------------------------------------------------------- /model_zoo/demo_bot.yml: -------------------------------------------------------------------------------- 1 | backend: !!python/unicode 'tensorflow' 2 | class_name: Sequential 3 | config: 4 | - class_name: ZeroPadding2D 5 | config: 6 | batch_input_shape: !!python/tuple [null, 7, 19, 19] 7 | data_format: !!python/unicode 'channels_first' 8 | dtype: !!python/unicode 'float32' 9 | name: !!python/unicode 'zero_padding2d_1' 10 | padding: !!python/tuple 11 | - !!python/tuple [3, 3] 12 | - !!python/tuple [3, 3] 13 | trainable: true 14 | - class_name: Conv2D 15 | config: 16 | activation: linear 17 | activity_regularizer: null 18 | bias_constraint: null 19 | bias_initializer: 20 | class_name: Zeros 21 | config: {} 22 | bias_regularizer: null 23 | data_format: !!python/unicode 'channels_first' 24 | dilation_rate: !!python/tuple [1, 1] 25 | filters: 64 26 | kernel_constraint: null 27 | kernel_initializer: 28 | class_name: VarianceScaling 29 | config: {distribution: !!python/unicode 'uniform', mode: !!python/unicode 'fan_avg', 30 | scale: 1.0, seed: null} 31 | kernel_regularizer: null 32 | kernel_size: !!python/tuple [7, 7] 33 | name: !!python/unicode 'conv2d_1' 34 | padding: !!python/unicode 'valid' 35 | strides: !!python/tuple [1, 1] 36 | trainable: true 37 | use_bias: true 38 | - class_name: Activation 39 | config: {activation: relu, name: !!python/unicode 'activation_1', trainable: true} 40 | - class_name: ZeroPadding2D 41 | config: 42 | data_format: !!python/unicode 'channels_first' 43 | name: !!python/unicode 'zero_padding2d_2' 44 | padding: !!python/tuple 45 | - !!python/tuple [2, 2] 46 | - !!python/tuple [2, 2] 47 | trainable: true 48 | - class_name: Conv2D 49 | config: 50 | activation: linear 51 | activity_regularizer: null 52 | bias_constraint: null 53 | bias_initializer: 54 | class_name: Zeros 55 | config: {} 56 | bias_regularizer: null 57 | data_format: !!python/unicode 'channels_first' 58 | dilation_rate: !!python/tuple [1, 1] 59 | filters: 64 60 | kernel_constraint: null 61 | kernel_initializer: 62 | class_name: VarianceScaling 63 | config: {distribution: !!python/unicode 'uniform', mode: !!python/unicode 'fan_avg', 64 | scale: 1.0, seed: null} 65 | kernel_regularizer: null 66 | kernel_size: !!python/tuple [5, 5] 67 | name: !!python/unicode 'conv2d_2' 68 | padding: !!python/unicode 'valid' 69 | strides: !!python/tuple [1, 1] 70 | trainable: true 71 | use_bias: true 72 | - class_name: Activation 73 | config: {activation: relu, name: !!python/unicode 'activation_2', trainable: true} 74 | - class_name: ZeroPadding2D 75 | config: 76 | data_format: !!python/unicode 'channels_first' 77 | name: !!python/unicode 'zero_padding2d_3' 78 | padding: !!python/tuple 79 | - !!python/tuple [2, 2] 80 | - !!python/tuple [2, 2] 81 | trainable: true 82 | - class_name: Conv2D 83 | config: 84 | activation: linear 85 | activity_regularizer: null 86 | bias_constraint: null 87 | bias_initializer: 88 | class_name: Zeros 89 | config: {} 90 | bias_regularizer: null 91 | data_format: !!python/unicode 'channels_first' 92 | dilation_rate: !!python/tuple [1, 1] 93 | filters: 64 94 | kernel_constraint: null 95 | kernel_initializer: 96 | class_name: VarianceScaling 97 | config: {distribution: !!python/unicode 'uniform', mode: !!python/unicode 'fan_avg', 98 | scale: 1.0, seed: null} 99 | kernel_regularizer: null 100 | kernel_size: !!python/tuple [5, 5] 101 | name: !!python/unicode 'conv2d_3' 102 | padding: !!python/unicode 'valid' 103 | strides: !!python/tuple [1, 1] 104 | trainable: true 105 | use_bias: true 106 | - class_name: Activation 107 | config: {activation: relu, name: !!python/unicode 'activation_3', trainable: true} 108 | - class_name: ZeroPadding2D 109 | config: 110 | data_format: !!python/unicode 'channels_first' 111 | name: !!python/unicode 'zero_padding2d_4' 112 | padding: !!python/tuple 113 | - !!python/tuple [2, 2] 114 | - !!python/tuple [2, 2] 115 | trainable: true 116 | - class_name: Conv2D 117 | config: 118 | activation: linear 119 | activity_regularizer: null 120 | bias_constraint: null 121 | bias_initializer: 122 | class_name: Zeros 123 | config: {} 124 | bias_regularizer: null 125 | data_format: !!python/unicode 'channels_first' 126 | dilation_rate: !!python/tuple [1, 1] 127 | filters: 48 128 | kernel_constraint: null 129 | kernel_initializer: 130 | class_name: VarianceScaling 131 | config: {distribution: !!python/unicode 'uniform', mode: !!python/unicode 'fan_avg', 132 | scale: 1.0, seed: null} 133 | kernel_regularizer: null 134 | kernel_size: !!python/tuple [5, 5] 135 | name: !!python/unicode 'conv2d_4' 136 | padding: !!python/unicode 'valid' 137 | strides: !!python/tuple [1, 1] 138 | trainable: true 139 | use_bias: true 140 | - class_name: Activation 141 | config: {activation: relu, name: !!python/unicode 'activation_4', trainable: true} 142 | - class_name: ZeroPadding2D 143 | config: 144 | data_format: !!python/unicode 'channels_first' 145 | name: !!python/unicode 'zero_padding2d_5' 146 | padding: !!python/tuple 147 | - !!python/tuple [2, 2] 148 | - !!python/tuple [2, 2] 149 | trainable: true 150 | - class_name: Conv2D 151 | config: 152 | activation: linear 153 | activity_regularizer: null 154 | bias_constraint: null 155 | bias_initializer: 156 | class_name: Zeros 157 | config: {} 158 | bias_regularizer: null 159 | data_format: !!python/unicode 'channels_first' 160 | dilation_rate: !!python/tuple [1, 1] 161 | filters: 48 162 | kernel_constraint: null 163 | kernel_initializer: 164 | class_name: VarianceScaling 165 | config: {distribution: !!python/unicode 'uniform', mode: !!python/unicode 'fan_avg', 166 | scale: 1.0, seed: null} 167 | kernel_regularizer: null 168 | kernel_size: !!python/tuple [5, 5] 169 | name: !!python/unicode 'conv2d_5' 170 | padding: !!python/unicode 'valid' 171 | strides: !!python/tuple [1, 1] 172 | trainable: true 173 | use_bias: true 174 | - class_name: Activation 175 | config: {activation: relu, name: !!python/unicode 'activation_5', trainable: true} 176 | - class_name: ZeroPadding2D 177 | config: 178 | data_format: !!python/unicode 'channels_first' 179 | name: !!python/unicode 'zero_padding2d_6' 180 | padding: !!python/tuple 181 | - !!python/tuple [2, 2] 182 | - !!python/tuple [2, 2] 183 | trainable: true 184 | - class_name: Conv2D 185 | config: 186 | activation: linear 187 | activity_regularizer: null 188 | bias_constraint: null 189 | bias_initializer: 190 | class_name: Zeros 191 | config: {} 192 | bias_regularizer: null 193 | data_format: !!python/unicode 'channels_first' 194 | dilation_rate: !!python/tuple [1, 1] 195 | filters: 32 196 | kernel_constraint: null 197 | kernel_initializer: 198 | class_name: VarianceScaling 199 | config: {distribution: !!python/unicode 'uniform', mode: !!python/unicode 'fan_avg', 200 | scale: 1.0, seed: null} 201 | kernel_regularizer: null 202 | kernel_size: !!python/tuple [5, 5] 203 | name: !!python/unicode 'conv2d_6' 204 | padding: !!python/unicode 'valid' 205 | strides: !!python/tuple [1, 1] 206 | trainable: true 207 | use_bias: true 208 | - class_name: Activation 209 | config: {activation: relu, name: !!python/unicode 'activation_6', trainable: true} 210 | - class_name: ZeroPadding2D 211 | config: 212 | data_format: !!python/unicode 'channels_first' 213 | name: !!python/unicode 'zero_padding2d_7' 214 | padding: !!python/tuple 215 | - !!python/tuple [2, 2] 216 | - !!python/tuple [2, 2] 217 | trainable: true 218 | - class_name: Conv2D 219 | config: 220 | activation: linear 221 | activity_regularizer: null 222 | bias_constraint: null 223 | bias_initializer: 224 | class_name: Zeros 225 | config: {} 226 | bias_regularizer: null 227 | data_format: !!python/unicode 'channels_first' 228 | dilation_rate: !!python/tuple [1, 1] 229 | filters: 32 230 | kernel_constraint: null 231 | kernel_initializer: 232 | class_name: VarianceScaling 233 | config: {distribution: !!python/unicode 'uniform', mode: !!python/unicode 'fan_avg', 234 | scale: 1.0, seed: null} 235 | kernel_regularizer: null 236 | kernel_size: !!python/tuple [5, 5] 237 | name: !!python/unicode 'conv2d_7' 238 | padding: !!python/unicode 'valid' 239 | strides: !!python/tuple [1, 1] 240 | trainable: true 241 | use_bias: true 242 | - class_name: Activation 243 | config: {activation: relu, name: !!python/unicode 'activation_7', trainable: true} 244 | - class_name: Flatten 245 | config: {name: !!python/unicode 'flatten_1', trainable: true} 246 | - class_name: Dense 247 | config: 248 | activation: linear 249 | activity_regularizer: null 250 | bias_constraint: null 251 | bias_initializer: 252 | class_name: Zeros 253 | config: {} 254 | bias_regularizer: null 255 | kernel_constraint: null 256 | kernel_initializer: 257 | class_name: VarianceScaling 258 | config: {distribution: !!python/unicode 'uniform', mode: !!python/unicode 'fan_avg', 259 | scale: 1.0, seed: null} 260 | kernel_regularizer: null 261 | name: !!python/unicode 'dense_1' 262 | trainable: true 263 | units: 1024 264 | use_bias: true 265 | - class_name: Activation 266 | config: {activation: relu, name: !!python/unicode 'activation_8', trainable: true} 267 | - class_name: Dense 268 | config: 269 | activation: linear 270 | activity_regularizer: null 271 | bias_constraint: null 272 | bias_initializer: 273 | class_name: Zeros 274 | config: {} 275 | bias_regularizer: null 276 | kernel_constraint: null 277 | kernel_initializer: 278 | class_name: VarianceScaling 279 | config: {distribution: !!python/unicode 'uniform', mode: !!python/unicode 'fan_avg', 280 | scale: 1.0, seed: null} 281 | kernel_regularizer: null 282 | name: !!python/unicode 'dense_2' 283 | trainable: true 284 | units: 361 285 | use_bias: true 286 | - class_name: Activation 287 | config: {activation: softmax, name: !!python/unicode 'activation_9', trainable: true} 288 | keras_version: 2.0.1 289 | -------------------------------------------------------------------------------- /model_zoo/demo_weights.hd5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/model_zoo/demo_weights.hd5 -------------------------------------------------------------------------------- /model_zoo/one_epoch_cnn_bot.yml: -------------------------------------------------------------------------------- 1 | class_name: Sequential 2 | config: 3 | - class_name: Convolution2D 4 | config: 5 | W_constraint: null 6 | W_regularizer: null 7 | activation: linear 8 | activity_regularizer: null 9 | b_constraint: null 10 | b_regularizer: null 11 | batch_input_shape: !!python/tuple [null, 7, 19, 19] 12 | border_mode: valid 13 | dim_ordering: th 14 | init: glorot_uniform 15 | input_dtype: float32 16 | name: convolution2d_1 17 | nb_col: 3 18 | nb_filter: 32 19 | nb_row: 3 20 | subsample: &id001 !!python/tuple [1, 1] 21 | trainable: true 22 | - class_name: Activation 23 | config: {activation: relu, name: activation_1, trainable: true} 24 | - class_name: Convolution2D 25 | config: 26 | W_constraint: null 27 | W_regularizer: null 28 | activation: linear 29 | activity_regularizer: null 30 | b_constraint: null 31 | b_regularizer: null 32 | border_mode: valid 33 | dim_ordering: th 34 | init: glorot_uniform 35 | name: convolution2d_2 36 | nb_col: 3 37 | nb_filter: 32 38 | nb_row: 3 39 | subsample: *id001 40 | trainable: true 41 | - class_name: Activation 42 | config: {activation: relu, name: activation_2, trainable: true} 43 | - class_name: MaxPooling2D 44 | config: 45 | border_mode: valid 46 | dim_ordering: th 47 | name: maxpooling2d_1 48 | pool_size: &id002 !!python/tuple [2, 2] 49 | strides: *id002 50 | trainable: true 51 | - class_name: Dropout 52 | config: {name: dropout_1, p: 0.25, trainable: true} 53 | - class_name: Flatten 54 | config: {name: flatten_1, trainable: true} 55 | - class_name: Dense 56 | config: {W_constraint: null, W_regularizer: null, activation: linear, activity_regularizer: null, 57 | b_constraint: null, b_regularizer: null, init: glorot_uniform, input_dim: null, 58 | name: dense_1, output_dim: 128, trainable: true} 59 | - class_name: Activation 60 | config: {activation: relu, name: activation_3, trainable: true} 61 | - class_name: Dropout 62 | config: {name: dropout_2, p: 0.5, trainable: true} 63 | - class_name: Dense 64 | config: {W_constraint: null, W_regularizer: null, activation: linear, activity_regularizer: null, 65 | b_constraint: null, b_regularizer: null, init: glorot_uniform, input_dim: null, 66 | name: dense_2, output_dim: 361, trainable: true} 67 | - class_name: Activation 68 | config: {activation: softmax, name: activation_4, trainable: true} 69 | -------------------------------------------------------------------------------- /model_zoo/one_epoch_cnn_weights.hd5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/model_zoo/one_epoch_cnn_weights.hd5 -------------------------------------------------------------------------------- /model_zoo/ten_epochs_cnn_bot.yml: -------------------------------------------------------------------------------- 1 | class_name: Sequential 2 | config: 3 | - class_name: Convolution2D 4 | config: 5 | W_constraint: null 6 | W_regularizer: null 7 | activation: linear 8 | activity_regularizer: null 9 | b_constraint: null 10 | b_regularizer: null 11 | batch_input_shape: !!python/tuple [null, 7, 19, 19] 12 | border_mode: valid 13 | dim_ordering: th 14 | init: glorot_uniform 15 | input_dtype: float32 16 | name: convolution2d_1 17 | nb_col: 3 18 | nb_filter: 32 19 | nb_row: 3 20 | subsample: &id001 !!python/tuple [1, 1] 21 | trainable: true 22 | - class_name: Activation 23 | config: {activation: relu, name: activation_1, trainable: true} 24 | - class_name: Convolution2D 25 | config: 26 | W_constraint: null 27 | W_regularizer: null 28 | activation: linear 29 | activity_regularizer: null 30 | b_constraint: null 31 | b_regularizer: null 32 | border_mode: valid 33 | dim_ordering: th 34 | init: glorot_uniform 35 | name: convolution2d_2 36 | nb_col: 3 37 | nb_filter: 32 38 | nb_row: 3 39 | subsample: *id001 40 | trainable: true 41 | - class_name: Activation 42 | config: {activation: relu, name: activation_2, trainable: true} 43 | - class_name: MaxPooling2D 44 | config: 45 | border_mode: valid 46 | dim_ordering: th 47 | name: maxpooling2d_1 48 | pool_size: &id002 !!python/tuple [2, 2] 49 | strides: *id002 50 | trainable: true 51 | - class_name: Dropout 52 | config: {name: dropout_1, p: 0.25, trainable: true} 53 | - class_name: Flatten 54 | config: {name: flatten_1, trainable: true} 55 | - class_name: Dense 56 | config: {W_constraint: null, W_regularizer: null, activation: linear, activity_regularizer: null, 57 | b_constraint: null, b_regularizer: null, init: glorot_uniform, input_dim: null, 58 | name: dense_1, output_dim: 128, trainable: true} 59 | - class_name: Activation 60 | config: {activation: relu, name: activation_3, trainable: true} 61 | - class_name: Dropout 62 | config: {name: dropout_2, p: 0.5, trainable: true} 63 | - class_name: Dense 64 | config: {W_constraint: null, W_regularizer: null, activation: linear, activity_regularizer: null, 65 | b_constraint: null, b_regularizer: null, init: glorot_uniform, input_dim: null, 66 | name: dense_2, output_dim: 361, trainable: true} 67 | - class_name: Activation 68 | config: {activation: softmax, name: activation_4, trainable: true} 69 | -------------------------------------------------------------------------------- /model_zoo/ten_epochs_cnn_weights.hd5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/model_zoo/ten_epochs_cnn_weights.hd5 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keras >= 2.0.0 2 | flask 3 | flask-cors 4 | future 5 | h5py 6 | tensorflow 7 | six 8 | -------------------------------------------------------------------------------- /run_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import argparse 4 | import yaml 5 | import os 6 | import webbrowser 7 | 8 | import tensorflow as tf 9 | from keras.models import model_from_yaml 10 | from betago.model import HTTPFrontend, KerasBot 11 | from betago.processor import SevenPlaneProcessor 12 | 13 | processor = SevenPlaneProcessor() 14 | 15 | bot_name = 'demo' 16 | model_file = 'model_zoo/' + bot_name + '_bot.yml' 17 | weight_file = 'model_zoo/' + bot_name + '_weights.hd5' 18 | 19 | 20 | with open(model_file, 'r') as f: 21 | yml = yaml.load(f) 22 | model = model_from_yaml(yaml.dump(yml)) 23 | # Note that in Keras 1.0 we have to recompile the model explicitly 24 | model.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy']) 25 | model.load_weights(weight_file) 26 | graph = tf.get_default_graph() 27 | 28 | parser = argparse.ArgumentParser() 29 | parser.add_argument('--host', default='localhost', help='host to listen to') 30 | parser.add_argument('--port', '-p', type=int, default=8080, 31 | help='Port the web server should listen on (default 8080).') 32 | args = parser.parse_args() 33 | 34 | # Open web frontend and serve model 35 | webbrowser.open('http://{}:{}/'.format(args.host, args.port), new=2) 36 | go_model = KerasBot(model=model, processor=processor) 37 | go_server = HTTPFrontend(bot=go_model, graph=graph, port=args.port) 38 | go_server.run() 39 | -------------------------------------------------------------------------------- /run_demo.sh: -------------------------------------------------------------------------------- 1 | cd .. 2 | virtualenv .betago 3 | . .betago/bin/activate 4 | cd betago 5 | python run_demo.py -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools import find_packages 3 | 4 | setup(name='betago', 5 | version='0.2', 6 | description='Create your own go bot from scratch. We are Lee Sedol!', 7 | url='http://github.com/maxpumperla/betago', 8 | download_url='https://github.com/maxpumperla/betago/tarball/0.2', 9 | author='Max Pumperla', 10 | author_email='max.pumperla@googlemail.com', 11 | install_requires=['keras', 'gomill', 'Flask>=0.10.1', 'Flask-Cors', 'future', 'h5py', 'six'], 12 | license='MIT', 13 | packages=find_packages(), 14 | zip_safe=False) 15 | -------------------------------------------------------------------------------- /test_samples.py: -------------------------------------------------------------------------------- 1 | ('KGS-2005-19-13941-.tar.gz', 3318) 2 | ('KGS-2008-19-14002-.tar.gz', 10020) 3 | ('KGS-2007-19-11644-.tar.gz', 1184) 4 | ('KGS-2008-19-14002-.tar.gz', 4880) 5 | ('KGS-2005-19-13941-.tar.gz', 1768) 6 | ('KGS-2014-19-13029-.tar.gz', 11504) 7 | ('KGS-2009-19-18837-.tar.gz', 9824) 8 | ('KGS-2008-19-14002-.tar.gz', 9826) 9 | ('KGS-2009-19-18837-.tar.gz', 14556) 10 | ('KGS-2009-19-18837-.tar.gz', 18489) 11 | ('KGS-2008-19-14002-.tar.gz', 13843) 12 | ('KGS-2003-19-7582-.tar.gz', 1130) 13 | ('KGS-2013-19-13783-.tar.gz', 2285) 14 | ('KGS-2007-19-11644-.tar.gz', 7556) 15 | ('KGS-2006-19-10388-.tar.gz', 2612) 16 | ('KGS-2010-19-17536-.tar.gz', 9028) 17 | ('KGS-2010-19-17536-.tar.gz', 14437) 18 | ('KGS-2009-19-18837-.tar.gz', 9711) 19 | ('KGS-2003-19-7582-.tar.gz', 5900) 20 | ('KGS-2013-19-13783-.tar.gz', 13314) 21 | ('KGS-2005-19-13941-.tar.gz', 3646) 22 | ('KGS-2005-19-13941-.tar.gz', 3924) 23 | ('KGS-2011-19-19099-.tar.gz', 2588) 24 | ('KGS-2008-19-14002-.tar.gz', 11953) 25 | ('KGS-2005-19-13941-.tar.gz', 8014) 26 | ('KGS-2009-19-18837-.tar.gz', 2369) 27 | ('KGS-2009-19-18837-.tar.gz', 13513) 28 | ('KGS-2009-19-18837-.tar.gz', 3639) 29 | ('KGS-2012-19-13665-.tar.gz', 12646) 30 | ('KGS-2007-19-11644-.tar.gz', 3313) 31 | ('KGS-2005-19-13941-.tar.gz', 7225) 32 | ('KGS-2010-19-17536-.tar.gz', 4811) 33 | ('KGS-2011-19-19099-.tar.gz', 6668) 34 | ('KGS-2004-19-12106-.tar.gz', 11567) 35 | ('KGS-2001-19-2298-.tar.gz', 904) 36 | ('KGS-2005-19-13941-.tar.gz', 7307) 37 | ('KGS-2014-19-13029-.tar.gz', 929) 38 | ('KGS-2004-19-12106-.tar.gz', 10633) 39 | ('KGS-2009-19-18837-.tar.gz', 14398) 40 | ('KGS-2009-19-18837-.tar.gz', 16993) 41 | ('KGS-2005-19-13941-.tar.gz', 4531) 42 | ('KGS-2010-19-17536-.tar.gz', 1238) 43 | ('KGS-2013-19-13783-.tar.gz', 9405) 44 | ('KGS-2010-19-17536-.tar.gz', 15115) 45 | ('KGS-2005-19-13941-.tar.gz', 12337) 46 | ('KGS-2012-19-13665-.tar.gz', 11286) 47 | ('KGS-2008-19-14002-.tar.gz', 7279) 48 | ('KGS-2007-19-11644-.tar.gz', 5998) 49 | ('KGS-2011-19-19099-.tar.gz', 13940) 50 | ('KGS-2002-19-3646-.tar.gz', 1354) 51 | ('KGS-2004-19-12106-.tar.gz', 3634) 52 | ('KGS-2011-19-19099-.tar.gz', 7863) 53 | ('KGS-2011-19-19099-.tar.gz', 14716) 54 | ('KGS-2013-19-13783-.tar.gz', 6791) 55 | ('KGS-2009-19-18837-.tar.gz', 15089) 56 | ('KGS-2010-19-17536-.tar.gz', 9547) 57 | ('KGS-2004-19-12106-.tar.gz', 10376) 58 | ('KGS-2006-19-10388-.tar.gz', 3436) 59 | ('KGS-2004-19-12106-.tar.gz', 9874) 60 | ('KGS-2009-19-18837-.tar.gz', 9171) 61 | ('KGS-2008-19-14002-.tar.gz', 13241) 62 | ('KGS-2008-19-14002-.tar.gz', 7086) 63 | ('KGS-2011-19-19099-.tar.gz', 17128) 64 | ('KGS-2014-19-13029-.tar.gz', 3868) 65 | ('KGS-2005-19-13941-.tar.gz', 2809) 66 | ('KGS-2007-19-11644-.tar.gz', 3540) 67 | ('KGS-2012-19-13665-.tar.gz', 457) 68 | ('KGS-2005-19-13941-.tar.gz', 9476) 69 | ('KGS-2010-19-17536-.tar.gz', 14082) 70 | ('KGS-2004-19-12106-.tar.gz', 4273) 71 | ('KGS-2006-19-10388-.tar.gz', 1242) 72 | ('KGS-2006-19-10388-.tar.gz', 9710) 73 | ('KGS-2010-19-17536-.tar.gz', 9946) 74 | ('KGS-2014-19-13029-.tar.gz', 2450) 75 | ('KGS-2011-19-19099-.tar.gz', 13796) 76 | ('KGS-2013-19-13783-.tar.gz', 8610) 77 | ('KGS-2011-19-19099-.tar.gz', 14749) 78 | ('KGS-2008-19-14002-.tar.gz', 6921) 79 | ('KGS-2010-19-17536-.tar.gz', 5508) 80 | ('KGS-2005-19-13941-.tar.gz', 1915) 81 | ('KGS-2014-19-13029-.tar.gz', 3608) 82 | ('KGS-2008-19-14002-.tar.gz', 2735) 83 | ('KGS-2012-19-13665-.tar.gz', 1881) 84 | ('KGS-2009-19-18837-.tar.gz', 10576) 85 | ('KGS-2003-19-7582-.tar.gz', 5020) 86 | ('KGS-2014-19-13029-.tar.gz', 3960) 87 | ('KGS-2012-19-13665-.tar.gz', 3445) 88 | ('KGS-2014-19-13029-.tar.gz', 4159) 89 | ('KGS-2012-19-13665-.tar.gz', 1169) 90 | ('KGS-2005-19-13941-.tar.gz', 9659) 91 | ('KGS-2008-19-14002-.tar.gz', 5670) 92 | ('KGS-2008-19-14002-.tar.gz', 1177) 93 | ('KGS-2005-19-13941-.tar.gz', 2866) 94 | ('KGS-2015-19-8133-.tar.gz', 7884) 95 | ('KGS-2010-19-17536-.tar.gz', 3940) 96 | ('KGS-2004-19-12106-.tar.gz', 7445) 97 | ('KGS-2011-19-19099-.tar.gz', 7763) 98 | ('KGS-2010-19-17536-.tar.gz', 13569) 99 | ('KGS-2006-19-10388-.tar.gz', 9588) 100 | ('KGS-2006-19-10388-.tar.gz', 5430) 101 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/tests/__init__.py -------------------------------------------------------------------------------- /tests/dataloader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/tests/dataloader/__init__.py -------------------------------------------------------------------------------- /tests/dataloader/goboard_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from betago.dataloader.goboard import GoBoard, from_string, to_string 4 | 5 | 6 | class GoBoardTest(unittest.TestCase): 7 | def test_is_move_legal(self): 8 | board = GoBoard() 9 | board.apply_move('b', (4, 4)) 10 | 11 | self.assertTrue(board.is_move_legal('b', (3, 3))) 12 | self.assertTrue(board.is_move_legal('w', (3, 3))) 13 | 14 | def test_is_move_legal_occupied(self): 15 | board = GoBoard() 16 | board.apply_move('b', (4, 4)) 17 | 18 | self.assertFalse(board.is_move_legal('b', (4, 4))) 19 | self.assertFalse(board.is_move_legal('w', (4, 4))) 20 | 21 | def test_is_move_legal_suicide(self): 22 | board = GoBoard() 23 | # Make a ponnuki. 24 | board.apply_move('b', (4, 4)) 25 | board.apply_move('b', (5, 5)) 26 | board.apply_move('b', (6, 4)) 27 | board.apply_move('b', (5, 3)) 28 | 29 | # White can't move in the center. 30 | self.assertFalse(board.is_move_legal('w', (5, 4))) 31 | # But Black can. 32 | self.assertTrue(board.is_move_legal('b', (5, 4))) 33 | 34 | def test_is_move_legal_capture_not_suicide(self): 35 | board = GoBoard() 36 | # Set up a ko. 37 | board.apply_move('b', (4, 4)) 38 | board.apply_move('b', (5, 5)) 39 | board.apply_move('b', (6, 4)) 40 | board.apply_move('b', (5, 3)) 41 | board.apply_move('w', (4, 5)) 42 | board.apply_move('w', (5, 6)) 43 | board.apply_move('w', (6, 5)) 44 | 45 | # Either can move in the center. 46 | self.assertTrue(board.is_move_legal('w', (5, 4))) 47 | self.assertTrue(board.is_move_legal('b', (5, 4))) 48 | 49 | def test_is_move_legal_should_not_mutate_board(self): 50 | board = GoBoard() 51 | board.apply_move('b', (4, 4)) 52 | 53 | board.is_move_legal('b', (4, 5)) 54 | 55 | # Check the board. 56 | self.assertFalse(board.is_move_on_board((4, 5))) 57 | # Check the go strings as well. 58 | group = board.go_strings[4, 4] 59 | self.assertEqual(1, group.get_num_stones()) 60 | self.assertEqual(4, group.get_num_liberties()) 61 | 62 | def test_is_move_legal_ko(self): 63 | board = GoBoard() 64 | # Set up a ko. 65 | board.apply_move('b', (4, 4)) 66 | board.apply_move('b', (5, 5)) 67 | board.apply_move('b', (6, 4)) 68 | board.apply_move('b', (5, 3)) 69 | board.apply_move('w', (4, 5)) 70 | board.apply_move('w', (5, 6)) 71 | board.apply_move('w', (6, 5)) 72 | # White captures. 73 | board.apply_move('w', (5, 4)) 74 | 75 | # Black can't fill in the ko. 76 | self.assertTrue(board.is_simple_ko('b', (5, 5))) 77 | self.assertFalse(board.is_move_legal('b', (5, 5))) 78 | # But white can. 79 | self.assertTrue(board.is_move_legal('w', (5, 5))) 80 | 81 | def test_from_string(self): 82 | board = from_string(''' 83 | .b... 84 | bb... 85 | ..... 86 | ..www 87 | ..w.. 88 | ''') 89 | 90 | self.assertEqual(5, board.board_size) 91 | self.assertNotIn((4, 0), board.board) 92 | self.assertEqual('b', board.board[4, 1]) 93 | self.assertNotIn((4, 2), board.board) 94 | self.assertNotIn((1, 0), board.board) 95 | self.assertNotIn((1, 1), board.board) 96 | self.assertEqual('w', board.board[1, 2]) 97 | self.assertEqual('w', board.board[0, 2]) 98 | 99 | def test_to_string(self): 100 | board = GoBoard(5) 101 | for move in [(3, 0), (3, 1), (4, 1)]: 102 | board.apply_move('b', move) 103 | for move in [(0, 2), (1, 2), (1, 3), (1, 4)]: 104 | board.apply_move('w', move) 105 | 106 | board_string = to_string(board) 107 | lines = board_string.split('\n') 108 | self.assertEqual([ 109 | '.b...', 110 | 'bb...', 111 | '.....', 112 | '..www', 113 | '..w..', 114 | ], lines) 115 | -------------------------------------------------------------------------------- /tests/gosgf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/tests/gosgf/__init__.py -------------------------------------------------------------------------------- /tests/gtp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/tests/gtp/__init__.py -------------------------------------------------------------------------------- /tests/gtp/board_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from betago.gtp.board import coords_to_gtp_position, gtp_position_to_coords 4 | 5 | 6 | class GTPCoordinateTest(unittest.TestCase): 7 | def test_coords_to_gtp_position(self): 8 | self.assertEqual('A1', coords_to_gtp_position((0, 0))) 9 | self.assertEqual('J3', coords_to_gtp_position((2, 8))) 10 | self.assertEqual('B15', coords_to_gtp_position((14, 1))) 11 | 12 | def test_gtp_position_to_coords(self): 13 | self.assertEqual((0, 0), gtp_position_to_coords('A1')) 14 | self.assertEqual((2, 8), gtp_position_to_coords('J3')) 15 | self.assertEqual((14, 1), gtp_position_to_coords('B15')) 16 | -------------------------------------------------------------------------------- /tests/gtp/command_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from betago.gtp import command 4 | 5 | 6 | class CommandTestCase(unittest.TestCase): 7 | def test_parse(self): 8 | command_string = 'play white D4' 9 | expected = command.Command( 10 | sequence=None, 11 | name='play', 12 | args=('white', 'D4'), 13 | ) 14 | self.assertEqual(expected, command.parse(command_string)) 15 | 16 | def test_parse_with_sequence_number(self): 17 | command_string = '999 play white D4' 18 | expected = command.Command( 19 | sequence=999, 20 | name='play', 21 | args=('white', 'D4'), 22 | ) 23 | self.assertEqual(expected, command.parse(command_string)) 24 | -------------------------------------------------------------------------------- /tests/gtp/response_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from betago.gtp import command, response 4 | 5 | 6 | class ResponseTestCase(unittest.TestCase): 7 | def setUp(self): 8 | self.cmd = command.Command(None, 'genmove', ('black',)) 9 | self.cmd_with_sequence = command.Command(99, 'genmove', ('black',)) 10 | 11 | def test_serialize(self): 12 | resp = response.success('D4') 13 | 14 | self.assertEqual('= D4\n\n', response.serialize(self.cmd, resp)) 15 | 16 | def test_serialize_error(self): 17 | resp = response.error('fail!') 18 | 19 | self.assertEqual('? fail!\n\n', response.serialize(self.cmd, resp)) 20 | 21 | def test_serialize_with_sequence(self): 22 | resp = response.success('D4') 23 | 24 | self.assertEqual('=99 D4\n\n', response.serialize(self.cmd_with_sequence, resp)) 25 | 26 | def test_serialize_error_with_sequence(self): 27 | resp = response.error('fail!') 28 | 29 | self.assertEqual('?99 fail!\n\n', response.serialize(self.cmd_with_sequence, resp)) 30 | -------------------------------------------------------------------------------- /tests/model_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import six 4 | 5 | from betago import model 6 | from betago.dataloader import goboard 7 | 8 | 9 | class ModelTestCase(unittest.TestCase): 10 | def test_all_empty_points(self): 11 | board = goboard.from_string(''' 12 | .b. 13 | bb. 14 | .ww 15 | ''') 16 | 17 | empty_points = model.all_empty_points(board) 18 | 19 | six.assertCountEqual( 20 | self, 21 | [(0, 0), (1, 2), (2, 0), (2, 2)], 22 | empty_points) 23 | 24 | def test_get_first_valid_move(self): 25 | board = goboard.from_string(''' 26 | .b. 27 | bb. 28 | .ww 29 | ''') 30 | candidates = [(0, 1), (1, 0), (2, 0), (2, 2)] 31 | 32 | self.assertEqual((2, 0), model.get_first_valid_move(board, 'b', candidates)) 33 | self.assertEqual((2, 2), model.get_first_valid_move(board, 'w', candidates)) 34 | -------------------------------------------------------------------------------- /tests/scoring_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from betago import scoring 4 | from betago.dataloader import goboard 5 | 6 | 7 | class ScoringTestCase(unittest.TestCase): 8 | def test_identify_territory(self): 9 | board = goboard.from_string(''' 10 | ...b..w.. 11 | ...b..w.. 12 | bbbb..w.. 13 | wwwww.www 14 | wwbbw..w. 15 | wb.bbwww. 16 | wbbbb.... 17 | ..b.b.... 18 | ..bbb.... 19 | ''') 20 | 21 | territory = scoring.evaluate_territory(board) 22 | 23 | self.assertEqual(8, territory.num_black_territory) 24 | self.assertEqual(6, territory.num_white_territory) 25 | self.assertEqual(20, territory.num_black_stones) 26 | self.assertEqual(20, territory.num_white_stones) 27 | self.assertEqual(27, territory.num_dame) 28 | 29 | self.assertIn((0, 0), territory.dame_points) 30 | self.assertNotIn((8, 0), territory.dame_points) 31 | -------------------------------------------------------------------------------- /train.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import importlib 3 | import multiprocessing 4 | import os 5 | import signal 6 | import sys 7 | import time 8 | 9 | import keras.backend 10 | import numpy as np 11 | import six.moves.queue as queue 12 | 13 | from betago.corpora import build_index, find_sgfs, load_index, store_index 14 | from betago.gosgf import Sgf_game 15 | from betago.dataloader import goboard 16 | from betago.processor import SevenPlaneProcessor 17 | from betago.training import TrainingRun 18 | 19 | 20 | def index(args): 21 | corpus_index = build_index(args.data, args.chunk_size) 22 | store_index(corpus_index, open(args.output, 'w')) 23 | 24 | 25 | def show(args): 26 | corpus_index = load_index(open(args.file)) 27 | print("Index contains %d chunks in %d physical files" % ( 28 | corpus_index.num_chunks, len(corpus_index.physical_files))) 29 | 30 | 31 | def _load_module_from_filename(filename): 32 | if sys.version_info < (3, 3): 33 | import imp 34 | return imp.load_source('dynamicmodule', filename) 35 | elif sys.version_info < (3, 5): 36 | from importlib.machinery import SourceFileLoader 37 | return SourceFileLoader('dynamicmodule', filename).load_module() 38 | else: 39 | import importlib.util 40 | spec = importlib.util.spec_from_file_location('dynamicmodule', filename) 41 | mod = importlib.util.module_from_spec(spec) 42 | spec.loader.exec_module(mod) 43 | return mod 44 | 45 | 46 | def _load_network_by_name(name): 47 | mod = None 48 | if os.path.exists(name): 49 | mod = _load_module_from_filename(name) 50 | else: 51 | try: 52 | mod = importlib.import_module('betago.' + name) 53 | except ImportError: 54 | mod = importlib.import_module(name) 55 | if not hasattr(mod, 'layers'): 56 | raise ImportError('%s does not defined a layers function.' % (name,)) 57 | return mod.layers 58 | 59 | 60 | def init(args): 61 | corpus_index = load_index(open(args.index)) 62 | layer_fn = _load_network_by_name(args.network) 63 | run = TrainingRun.create(args.progress, corpus_index, layer_fn) 64 | 65 | 66 | def _disable_keyboard_interrupt(): 67 | signal.signal(signal.SIGINT, signal.SIG_IGN) 68 | 69 | 70 | def _prepare_training_data_single_process(worker_idx, chunk, corpus_index, output_q, stop_q): 71 | # Make sure ^C gets handled in the main process. 72 | _disable_keyboard_interrupt() 73 | processor = SevenPlaneProcessor() 74 | 75 | chunk = corpus_index.get_chunk(chunk) 76 | xs, ys = [], [] 77 | for board, next_color, next_move in chunk: 78 | if not stop_q.empty(): 79 | print("Got stop signal, aborting.") 80 | return 81 | feature, label = processor.feature_and_label(next_color, next_move, board, 82 | processor.num_planes) 83 | xs.append(feature) 84 | ys.append(label) 85 | X = np.array(xs) 86 | # one-hot encode the moves 87 | nb_classes = 19 * 19 88 | Y = np.zeros((len(ys), nb_classes)) 89 | for i, y in enumerate(ys): 90 | Y[i][y] = 1 91 | output_q.put((worker_idx, X, Y)) 92 | output_q.close() 93 | 94 | 95 | def prepare_training_data(num_workers, next_chunk, corpus_index, output_q, stop_q): 96 | # Make sure ^C gets handled in the main process. 97 | _disable_keyboard_interrupt() 98 | stopped = False 99 | 100 | while True: 101 | if not stop_q.empty(): 102 | output_q.close() 103 | output_q.join_thread() 104 | stop_q.close() 105 | stop_q.join_thread() 106 | return 107 | chunks_to_process = [] 108 | for _ in range(num_workers): 109 | chunks_to_process.append(next_chunk) 110 | next_chunk = (next_chunk + 1) % corpus_index.num_chunks 111 | workers = [] 112 | inter_q = multiprocessing.Queue() 113 | for i, chunk in enumerate(chunks_to_process): 114 | workers.append(multiprocessing.Process( 115 | target=_prepare_training_data_single_process, 116 | args=(i, chunk, corpus_index, inter_q, stop_q))) 117 | for worker in workers: 118 | worker.start() 119 | results = [] 120 | while len(results) < len(workers): 121 | try: 122 | results.append(inter_q.get(block=True, timeout=1)) 123 | except queue.Empty: 124 | if not stop_q.empty(): 125 | stopped = True 126 | break 127 | for worker in workers: 128 | worker.join() 129 | inter_q.close() 130 | inter_q.join_thread() 131 | if stopped: 132 | output_q.close() 133 | return 134 | assert len(results) == len(workers) 135 | results.sort() 136 | for _, X, Y in results: 137 | output_q.put((X, Y)) 138 | 139 | 140 | def train(args): 141 | corpus_index = load_index(open(args.index)) 142 | print("Index contains %d chunks in %d physical files" % ( 143 | corpus_index.num_chunks, len(corpus_index.physical_files))) 144 | if not os.path.exists(args.progress): 145 | print('%s does not exist. Run train.py init first.' % (args.progress,)) 146 | else: 147 | run = TrainingRun.load(args.progress) 148 | 149 | q = multiprocessing.Queue(maxsize=2 * args.workers) 150 | stop_q = multiprocessing.Queue() 151 | p = multiprocessing.Process(target=prepare_training_data, 152 | args=(args.workers, run.chunks_completed, corpus_index, q, stop_q)) 153 | p.start() 154 | try: 155 | while True: 156 | print("Waiting for prepared training chunk...") 157 | wait_start_ts = time.time() 158 | X, Y = q.get() 159 | wait_end_ts = time.time() 160 | print("Idle %.1f seconds" % (wait_end_ts - wait_start_ts,)) 161 | print("Training epoch %d chunk %d/%d..." % ( 162 | run.epochs_completed + 1, 163 | run.chunks_completed + 1, 164 | run.num_chunks)) 165 | run.model.fit(X, Y, epochs=1) 166 | run.complete_chunk() 167 | finally: 168 | # Drain the receive queue. 169 | while not q.empty(): 170 | q.get() 171 | q.close() 172 | q.join_thread() 173 | print("Shutting down workers, please wait...") 174 | stop_q.put(1) 175 | stop_q.close() 176 | p.join() 177 | 178 | 179 | def export(args): 180 | run = TrainingRun.load(args.progress) 181 | 182 | model_file = args.bot + '_bot.yml' 183 | weight_file = args.bot + '_weights.hd5' 184 | run.model.save_weights(weight_file, overwrite=True) 185 | with open(model_file, 'w') as yml: 186 | yml.write(run.model.to_yaml()) 187 | 188 | 189 | def main(): 190 | parser = argparse.ArgumentParser() 191 | subparsers = parser.add_subparsers() 192 | 193 | index_parser = subparsers.add_parser('index', help='Build an index for a corpus.') 194 | index_parser.set_defaults(command='index') 195 | index_parser.add_argument('--output', '-o', required=True, 196 | help='Path to store the index.') 197 | index_parser.add_argument('--data', '-d', required=True, 198 | help='Directory or archive containing SGF files.') 199 | index_parser.add_argument('--chunk-size', '-c', type=int, default=20000, 200 | help='Number of examples per training chunk.') 201 | 202 | show_parser = subparsers.add_parser('show', help='Show a summary of an index.') 203 | show_parser.set_defaults(command='show') 204 | show_parser.add_argument('--file', '-f', required=True, help='Index file.') 205 | 206 | init_parser = subparsers.add_parser('init', help='Start training.') 207 | init_parser.set_defaults(command='init') 208 | init_parser.add_argument('--index', '-i', required=True, help='Index file.') 209 | init_parser.add_argument('--progress', '-p', required=True, help='Progress file.') 210 | init_parser.add_argument('--network', '-n', required=True, 211 | help='Python module that defines the network architecture, ' 212 | 'e.g. "networks.small"') 213 | 214 | train_parser = subparsers.add_parser('train', help='Do some training.') 215 | train_parser.set_defaults(command='train') 216 | train_parser.add_argument('--index', '-i', required=True, help='Index file.') 217 | train_parser.add_argument('--progress', '-p', required=True, help='Progress file.') 218 | train_parser.add_argument('--workers', '-w', type=int, default=1, 219 | help='Number of workers to use for preprocessing boards.') 220 | 221 | export_parser = subparsers.add_parser('export', help='Export a bot from a training run.') 222 | export_parser.set_defaults(command='export') 223 | export_parser.add_argument('--progress', '-p', required=True, help='Progress file.') 224 | export_parser.add_argument('--bot', '-b', help='Bot file name.') 225 | 226 | args = parser.parse_args() 227 | 228 | if args.command == 'index': 229 | index(args) 230 | elif args.command == 'show': 231 | show(args) 232 | elif args.command == 'init': 233 | init(args) 234 | elif args.command == 'train': 235 | train(args) 236 | elif args.command == 'export': 237 | export(args) 238 | 239 | if __name__ == '__main__': 240 | main() 241 | -------------------------------------------------------------------------------- /ui/Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | module.exports = function(grunt) { 3 | 'use strict'; 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | pkg: grunt.file.readJSON('package.json'), 8 | 9 | // Do hinting/linting with 'grunt jshint' 10 | jshint: { 11 | files: ['Gruntfile.js', 'JGO/**/*.js', 'test/**/*.js'], 12 | options: { 13 | jshintrc: true 14 | } 15 | }, 16 | 17 | // Use shell command until jsdoc support gets to 3.3.0 (without Java) 18 | shell: { 19 | makeDocs: { 20 | //command: 'echo <%= grunt.file.expand("JGO/*.js").join(" ") %>', 21 | command: 'jsdoc -d doc JGO', // JSDoc doesn't support expansion 22 | options: { 23 | stdout: true, 24 | stderr: true 25 | } 26 | } 27 | }, 28 | 29 | browserify: { 30 | dist: { 31 | src: 'main.js', 32 | dest: 'dist/<%= pkg.name %>-<%= pkg.version %>.js' 33 | } 34 | }, 35 | 36 | concat: { 37 | options: { 38 | separator: '\n', 39 | banner: '/*! <%= pkg.name %> <%= pkg.version %>, (c) <%= pkg.author %>. ' + 40 | 'Licensed under <%= pkg.license %>, see <%= pkg.homepage %> for details. */\n' 41 | }, 42 | dist: { 43 | src: [ 'JGO/jgoinit.js', 'JGO/*.js' ], 44 | dest: 'dist/<%= pkg.name %>-<%= pkg.version %>.js', 45 | } 46 | }, 47 | 48 | uglify: { 49 | options: { 50 | banner: '<%= concat.options.banner %>' 51 | }, 52 | dist: { 53 | files: { 54 | 'dist/<%= pkg.name %>-<%= pkg.version %>.min.js': ['<%= concat.dist.dest %>'] 55 | } 56 | } 57 | }, 58 | 59 | copy: { 60 | main: { 61 | src: 'dist/<%= pkg.name %>-<%= pkg.version %>.js', 62 | dest: 'dist/<%= pkg.name %>-latest.js' 63 | } 64 | } 65 | }); 66 | 67 | // Load the plugin that provides the "uglify" task. 68 | grunt.loadNpmTasks('grunt-contrib-concat'); 69 | grunt.loadNpmTasks('grunt-contrib-uglify'); 70 | grunt.loadNpmTasks('grunt-contrib-jshint'); 71 | grunt.loadNpmTasks('grunt-contrib-copy'); 72 | //grunt.loadNpmTasks('grunt-jsdoc'); 73 | grunt.loadNpmTasks('grunt-shell'); 74 | grunt.loadNpmTasks('grunt-browserify'); 75 | 76 | // Default task(s). 77 | //grunt.registerTask('default', ['jshint', 'browserify', 'uglify', 'copy']); 78 | grunt.registerTask('default', ['browserify', 'copy']); 79 | }; 80 | -------------------------------------------------------------------------------- /ui/JGO/auto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //var request = require('superagent'); 4 | var C = require('./constants'); 5 | 6 | /** 7 | * Automatic div module. 8 | * @module autodiv 9 | */ 10 | 11 | function parseMarkup(str) { 12 | var lines = str.split('\n'), data = []; 13 | 14 | // Handle div contents as diagram contents 15 | for(var i = 0, len = lines.length; i < len; ++i) { 16 | var elems = [], line = lines[i]; 17 | 18 | for(var j = 0, len2 = line.length; j < len2; ++j) { 19 | switch(line[j]) { 20 | case '.': 21 | elems.push({type: C.CLEAR}); break; 22 | case 'o': 23 | elems.push({type: C.WHITE}); break; 24 | case 'x': 25 | elems.push({type: C.BLACK}); break; 26 | case ' ': 27 | break; // ignore whitespace 28 | default: // assume marker 29 | if(!elems.length) break; // no intersection yet 30 | // Append to mark so x123 etc. are possible 31 | if(elems[elems.length - 1].mark) 32 | elems[elems.length - 1].mark += line[j]; 33 | else 34 | elems[elems.length - 1].mark = line[j]; 35 | } 36 | } 37 | 38 | if(elems.length) data.push(elems); 39 | } 40 | 41 | return data; 42 | } 43 | 44 | // Array of loaded boards 45 | //var boards = []; 46 | 47 | // Available attributes: 48 | // data-jgostyle: Evaluated and used as board style 49 | // data-jgosize: Used as board size unless data-jgosgf is defined 50 | // data-jgoview: Used to define viewport 51 | function process(JGO, div) { 52 | // Handle special jgo-* attributes 53 | var style, width, height, TL, BR; // last two are viewport 54 | 55 | if(div.getAttribute('data-jgostyle')) { 56 | /*jshint evil:true */ 57 | style = eval(div.getAttribute('data-jgostyle')); 58 | } else style = JGO.BOARD.medium; 59 | 60 | if(div.getAttribute('data-jgosize')) { 61 | var size = div.getAttribute('data-jgosize'); 62 | 63 | if(size.indexOf('x') != -1) { 64 | width = parseInt(size.substring(0, size.indexOf('x'))); 65 | height = parseInt(size.substr(size.indexOf('x')+1)); 66 | } else width = height = parseInt(size); 67 | } 68 | 69 | //if(div.getAttribute('data-jgosgf')) 70 | 71 | var data = parseMarkup(div.innerHTML); 72 | div.innerHTML = ''; 73 | 74 | if(!width) { // Size still missing 75 | if(!data.length) return; // no size or data, no board 76 | 77 | height = data.length; 78 | width = data[0].length; 79 | } 80 | 81 | var jboard = new JGO.Board(width, height); 82 | var jsetup = new JGO.Setup(jboard, style); 83 | 84 | if(div.getAttribute('data-jgoview')) { 85 | var tup = div.getAttribute('data-jgoview').split('-'); 86 | TL = jboard.getCoordinate(tup[0]); 87 | BR = jboard.getCoordinate(tup[1]); 88 | } else { 89 | TL = new JGO.Coordinate(0,0); 90 | BR = new JGO.Coordinate(width-1, height-1); 91 | } 92 | 93 | jsetup.view(TL.i, TL.j, width-TL.i, height-TL.j); 94 | 95 | var c = new JGO.Coordinate(); 96 | 97 | for(c.j = TL.j; c.j <= BR.j; ++c.j) { 98 | for(c.i = TL.i; c.i <= BR.i; ++c.i) { 99 | var elem = data[c.j - TL.j][c.i - TL.i]; 100 | jboard.setType(c, elem.type); 101 | if(elem.mark) jboard.setMark(c, elem.mark); 102 | } 103 | } 104 | 105 | jsetup.create(div); 106 | } 107 | 108 | /** 109 | * Find all div elements with class 'jgoboard' and initialize them. 110 | */ 111 | exports.init = function(document, JGO) { 112 | var matches = document.querySelectorAll('div.jgoboard'); 113 | 114 | for(var i = 0, len = matches.length; i < len; ++i) 115 | process(JGO, matches[i]); 116 | }; 117 | -------------------------------------------------------------------------------- /ui/JGO/board.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Coordinate = require('./coordinate'); 4 | var C = require('./constants'); 5 | var util = require('./util'); 6 | 7 | /** 8 | * Go board class for storing intersection states. Also has listeners that 9 | * are notified on any changes to the board via setType() and setMark(). 10 | * 11 | * @param {int} width The width of the board 12 | * @param {int} [height] The height of the board 13 | * @constructor 14 | */ 15 | var Board = function(width, height) { 16 | this.width = width; 17 | 18 | if(height !== undefined) 19 | this.height = height; 20 | else { //noinspection JSSuspiciousNameCombination 21 | this.height = this.width; 22 | } 23 | 24 | this.listeners = []; 25 | 26 | this.stones = []; 27 | this.marks = []; 28 | 29 | // Initialize stones and marks 30 | for(var i=0; i0) 215 | coordinates.push(new Coordinate(i-1, j)); 216 | if(i+10) 219 | coordinates.push(new Coordinate(i, j-1)); 220 | if(j+1=0; i--) { 38 | var item = this.parent.changes[i]; 39 | 40 | if('mark' in item) 41 | this.setMark(item.c, C.MARK.NONE); 42 | } 43 | }; 44 | 45 | /** 46 | * Helper method to make changes to a board while saving them in the node. 47 | * 48 | * @param {Object} c Coordinate or array of them. 49 | * @param {int} val Type. 50 | */ 51 | Node.prototype.setType = function(c, val) { 52 | if(c instanceof Array) { 53 | for(var i=0, len=c.length; i=0; i--) { 101 | var item = this.changes[i]; 102 | 103 | if('type' in item) 104 | this.jboard.setType(item.c, item.old); 105 | else 106 | this.jboard.setMark(item.c, item.old); 107 | } 108 | }; 109 | 110 | module.exports = Node; 111 | -------------------------------------------------------------------------------- /ui/JGO/notifier.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * A change notifier class that can listen to changes in a Board and keep 5 | * multiple Canvas board views up to date. 6 | * 7 | * @param {Board} jboard The board to listen to. 8 | * @constructor 9 | */ 10 | var Notifier = function(jboard) { 11 | this.updateScheduled = false; // set on first change 12 | this.canvases = []; // canvases to notify on changes 13 | this.board = jboard; 14 | 15 | this.changeFunc = function(ev) { 16 | var coord = ev.coordinate; 17 | 18 | if(this.updateScheduled) { // update already scheduled 19 | this.min.i = Math.min(this.min.i, coord.i); 20 | this.min.j = Math.min(this.min.j, coord.j); 21 | this.max.i = Math.max(this.max.i, coord.i); 22 | this.max.j = Math.max(this.max.j, coord.j); 23 | return; 24 | } 25 | 26 | this.min = coord.copy(); 27 | this.max = coord.copy(); 28 | this.updateScheduled = true; 29 | 30 | setTimeout(function() { // schedule update in the end 31 | for(var c=0; c= this.current.children.length) 81 | return null; 82 | 83 | this.current = this.current.children[variation]; 84 | this.current.apply(this.jboard); 85 | 86 | return this.current; 87 | }; 88 | 89 | /** 90 | * Back up a node in the game tree. 91 | * 92 | * @returns {Node} New current node or null if at the beginning of game tree. 93 | */ 94 | Record.prototype.previous = function() { 95 | if(this.current === null || this.current.parent === null) 96 | return null; // empty or no parent 97 | 98 | this.current.revert(this.jboard); 99 | this.current = this.current.parent; 100 | 101 | return this.current; 102 | }; 103 | 104 | /** 105 | * Get current variation number (zero-based). 106 | * 107 | * @returns {int} Current variations. 108 | */ 109 | Record.prototype.getVariation = function() { 110 | if(this.current === null || this.current.parent === null) 111 | return 0; 112 | return this.current.parent.children.indexOf(this.current); 113 | }; 114 | 115 | /** 116 | * Go to a variation. Uses previous() and next(). 117 | * 118 | * @param {int} [variation] parameter to specify which variation to select, if there are several branches. 119 | */ 120 | Record.prototype.setVariation = function(variation) { 121 | if(this.previous() === null) 122 | return null; 123 | return this.next(variation); 124 | }; 125 | 126 | /** 127 | * Get number of variations for current node. 128 | * 129 | * @returns {int} Number of variations. 130 | */ 131 | Record.prototype.getVariations = function() { 132 | if(this.current === null || this.current.parent === null) 133 | return 1; 134 | 135 | return this.current.parent.children.length; // "nice" 136 | }; 137 | 138 | /** 139 | * Go to the beginning of the game tree. 140 | * 141 | * @returns {Node} New current node. 142 | */ 143 | Record.prototype.first = function() { 144 | this.current = this.root; 145 | this.jboard.clear(); 146 | 147 | if(this.current !== null) 148 | this.current.apply(this.jboard); 149 | 150 | return this.current; 151 | }; 152 | 153 | /** 154 | * Create a snapshot of current Record state. Will contain board state and 155 | * current node. 156 | * 157 | * @returns Snapshot to be used with restoreSnapshot(). 158 | */ 159 | Record.prototype.createSnapshot = function() { 160 | return {jboard: this.jboard.getRaw(), current: this.current}; 161 | }; 162 | 163 | /** 164 | * Restore the Record to the state contained in snapshot. Use only if you 165 | * REALLY know what you are doing, this is mainly for creating Record 166 | * quickly from SGF. 167 | * 168 | * @param {Object} raw Snapshot created with createSnapshot(). 169 | */ 170 | Record.prototype.restoreSnapshot = function(raw) { 171 | this.jboard.setRaw(raw.jboard); 172 | this.current = raw.current; 173 | }; 174 | 175 | /** 176 | * Normalize record so the longest variation is the first. 177 | * 178 | * @param {Node} node The node to start with. Defaults to root if unset. 179 | * @returns int Length of longest subsequence. 180 | */ 181 | Record.prototype.normalize = function(node) { 182 | var i, len, maxLen = 0, maxI = 0; 183 | 184 | if(!node) node = this.getRootNode(); 185 | 186 | for(i=0; i 2 | 3 | 4 | 5 | 6 | BetaGo user interface 7 | 17 | 18 | 19 | 20 | 21 |

BetaGo

22 |
23 |
24 |

Click to play.

25 |
26 | 27 | 28 | 29 | 30 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /ui/large/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/large/black.png -------------------------------------------------------------------------------- /ui/large/board.js: -------------------------------------------------------------------------------- 1 | // Import or create JGO namespace 2 | var JGO = JGO || {}; 3 | 4 | JGO.BOARD = JGO.BOARD || {}; 5 | 6 | JGO.BOARD.large = { 7 | textures: { 8 | black: 'large/black.png', 9 | white: 'large/white.png', 10 | shadow:'large/shadow.png', 11 | board: 'large/shinkaya.jpg' 12 | }, 13 | 14 | // Margins around the board, both on normal edges and clipped ones 15 | margin: {normal: 40, clipped: 40}, 16 | 17 | // Shadow color, blur and offset 18 | boardShadow: {color: '#ffe0a8', blur: 30, offX: 5, offY: 5}, 19 | 20 | // Lighter border around the board makes it more photorealistic 21 | border: {color: 'rgba(255, 255, 255, 0.3)', lineWidth: 2}, 22 | 23 | // Amount of "extra wood" around the grid where stones lie 24 | padding: {normal: 20, clipped: 10}, 25 | 26 | // Grid color and size, line widths 27 | grid: {color: '#202020', x: 50, y: 50, smooth: 0.0, 28 | borderWidth: 1.5, lineWidth: 1.2}, 29 | 30 | // Star point radius 31 | stars: {radius: 3}, 32 | 33 | // Coordinate color and font 34 | coordinates: {color: '#808080', font: 'normal 18px sanf-serif'}, 35 | 36 | // Stone radius and alpha for semi-transparent stones 37 | stone: {radius: 24, dimAlpha:0.6}, 38 | 39 | // Shadow offset from center 40 | shadow: {xOff: -2, yOff: 2}, 41 | 42 | // Mark base size and line width, color and font settings 43 | mark: {lineWidth: 1.5, blackColor: 'white', whiteColor: 'black', 44 | clearColor: 'black', font: 'normal 24px sanf-serif'} 45 | }; 46 | 47 | JGO.BOARD.largeWalnut = JGO.util.extend(JGO.util.extend({}, JGO.BOARD.large), { 48 | textures: {board: 'large/walnut.jpg', shadow: 'large/shadow_dark.png'}, 49 | boardShadow: {color: '#e2baa0'}, 50 | grid: {color: '#101010', borderWidth: 1.8, lineWidth: 1.5} 51 | }); 52 | 53 | JGO.BOARD.largeBW = JGO.util.extend(JGO.util.extend({}, JGO.BOARD.large), { 54 | textures: false 55 | }); 56 | -------------------------------------------------------------------------------- /ui/large/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/large/shadow.png -------------------------------------------------------------------------------- /ui/large/shadow_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/large/shadow_dark.png -------------------------------------------------------------------------------- /ui/large/shinkaya.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/large/shinkaya.jpg -------------------------------------------------------------------------------- /ui/large/walnut.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/large/walnut.jpg -------------------------------------------------------------------------------- /ui/large/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/large/white.png -------------------------------------------------------------------------------- /ui/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var JGO = require('./JGO'); 3 | window.JGO = JGO; // expose as global object 4 | -------------------------------------------------------------------------------- /ui/medium/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/medium/black.png -------------------------------------------------------------------------------- /ui/medium/board.js: -------------------------------------------------------------------------------- 1 | // Import or create JGO namespace 2 | var JGO = JGO || {}; 3 | 4 | JGO.BOARD = JGO.BOARD || {}; 5 | 6 | JGO.BOARD.medium = { 7 | textures: { 8 | black: 'medium/black.png', 9 | white: 'medium/white.png', 10 | shadow:'medium/shadow.png', 11 | board: 'medium/shinkaya.jpg' 12 | }, 13 | 14 | // Margins around the board, both on normal edges and clipped ones 15 | margin: {normal: 20, clipped: 20}, 16 | 17 | // Shadow color, blur and offset 18 | boardShadow: {color: '#ffe0a8', blur: 15, offX: 2.5, offY: 2.5}, 19 | 20 | // Lighter border around the board makes it more photorealistic 21 | border: {color: 'rgba(255, 255, 255, 0.3)', lineWidth: 2}, 22 | 23 | // Amount of "extra wood" around the grid where stones lie 24 | padding: {normal: 10, clipped: 5}, 25 | 26 | // Grid color and size, line widths 27 | grid: {color: '#202020', x: 25, y: 25, smooth: 0, 28 | borderWidth: 1.2, lineWidth: 0.9}, 29 | 30 | // Star point radius 31 | stars: {radius: 2.5}, 32 | 33 | // Coordinate color and font 34 | coordinates: {color: '#808080', font: 'normal 12px sanf-serif'}, 35 | 36 | // Stone radius and alpha for semi-transparent stones 37 | stone: {radius: 12, dimAlpha:0.6}, 38 | 39 | // Shadow offset from center 40 | shadow: {xOff: -1, yOff: 1}, 41 | 42 | // Mark base size and line width, color and font settings 43 | mark: {lineWidth: 1.0, blackColor: 'white', whiteColor: 'black', 44 | clearColor: 'black', font: 'normal 12px sanf-serif'} 45 | }; 46 | 47 | JGO.BOARD.mediumWalnut = JGO.util.extend(JGO.util.extend({}, JGO.BOARD.medium), { 48 | textures: {board: 'medium/walnut.jpg', shadow: 'medium/shadow_dark.png'}, 49 | boardShadow: {color: '#e2baa0'}, 50 | grid: {color: '#101010', borderWidth: 1.4, lineWidth: 1.1} 51 | }); 52 | 53 | JGO.BOARD.mediumBW = JGO.util.extend(JGO.util.extend({}, JGO.BOARD.medium), { 54 | textures: false 55 | }); 56 | -------------------------------------------------------------------------------- /ui/medium/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/medium/shadow.png -------------------------------------------------------------------------------- /ui/medium/shadow_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/medium/shadow_dark.png -------------------------------------------------------------------------------- /ui/medium/shinkaya.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/medium/shinkaya.jpg -------------------------------------------------------------------------------- /ui/medium/walnut.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/medium/walnut.jpg -------------------------------------------------------------------------------- /ui/medium/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxpumperla/betago/ff06b467e16d7a7a22555d14181b723d853e1a70/ui/medium/white.png -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jgoboard", 3 | "version": "3.4.2", 4 | "description": "jGoBoard javascript go board library", 5 | "main": "src/jgoinit.js", 6 | "directories": { 7 | "doc": "doc" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/jokkebk/jgoboard.git" 15 | }, 16 | "keywords": [ 17 | "go", 18 | "baduk", 19 | "go", 20 | "board", 21 | "goban", 22 | "weiqi", 23 | "javascript" 24 | ], 25 | "author": "Joonas Pihlajamaa", 26 | "homepage": "http://jgoboard.com", 27 | "license": "CC-BY-NC-4.0", 28 | "devDependencies": { 29 | "browserify": "latest", 30 | "grunt": "^0.4.5", 31 | "grunt-browserify": "^3.5.0", 32 | "grunt-contrib-concat": "^0.5.0", 33 | "grunt-contrib-copy": "^0.5.0", 34 | "grunt-contrib-jshint": "^0.10.0", 35 | "grunt-contrib-uglify": "^0.5.1", 36 | "grunt-jsdoc": "~0.5.1", 37 | "grunt-shell": "^1.1.1" 38 | }, 39 | "dependencies": { 40 | "superagent": "^5.1.0" 41 | } 42 | } 43 | --------------------------------------------------------------------------------