├── boardserver ├── __init__.py └── server.py ├── .gitignore ├── setup.py ├── bin └── board-serve.py ├── LICENSE └── README.rst /boardserver/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.egg-info 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from setuptools import setup 3 | 4 | setup( 5 | name='BoardServer', 6 | version='0.1dev', 7 | author='Jeff Bradberry', 8 | author_email='jeff.bradberry@gmail.com', 9 | packages=['boardserver'], 10 | scripts=['bin/board-serve.py'], 11 | entry_points={'jrb_board.games': []}, 12 | install_requires=['gevent', 'six'], 13 | license='LICENSE', 14 | description="A generic board game socket server.", 15 | ) 16 | -------------------------------------------------------------------------------- /bin/board-serve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import absolute_import 3 | import sys 4 | from pkg_resources import iter_entry_points 5 | from boardserver import server 6 | 7 | 8 | board_plugins = dict( 9 | (ep.name, ep.load()) 10 | for ep in iter_entry_points('jrb_board.games') 11 | ) 12 | 13 | 14 | args = sys.argv[1:] 15 | addr, port = None, None 16 | 17 | board = board_plugins[args[0]] 18 | 19 | if len(args) > 1: 20 | addr = args[1] 21 | if len(args) > 2: 22 | port = int(args[2]) 23 | 24 | 25 | api = server.Server(board(), addr, port) 26 | api.run() 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Jeff Bradberry 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | boardgame-socketserver 2 | ====================== 3 | 4 | A server for pluggable board game implementations, implemented using gevent. 5 | 6 | The server included here is designed to work with 7 | `jbradberry/boardgame-socketplayer 8 | `_ and the Monte 9 | Carlo Tree Search implementation `jbradberry/mcts 10 | `_. 11 | 12 | 13 | Requirements 14 | ------------ 15 | 16 | * Python 2.7, 3.5+; PyPy is not supported by the server 17 | * gevent 18 | * six 19 | 20 | 21 | Getting Started 22 | --------------- 23 | 24 | To set up your local environment you should create a virtualenv and 25 | install everything into it. :: 26 | 27 | $ mkvirtualenv boardgames 28 | 29 | Pip install this repo, either from a local copy, :: 30 | 31 | $ pip install -e boardgame-socketserver 32 | 33 | or from github, :: 34 | 35 | $ pip install git+https://github.com/jbradberry/boardgame-socketserver 36 | 37 | To run the server with (for example) `Ultimate Tic Tac Toe 38 | `_ :: 39 | 40 | $ board-serve.py t3 41 | 42 | Optionally, the server ip address and port number can be added :: 43 | 44 | $ board-serve.py t3 0.0.0.0 45 | $ board-serve.py t3 0.0.0.0 8000 46 | 47 | To connect a client as a human player, using `boardgame-socketplayer 48 | `_ :: 49 | 50 | $ board-play.py t3 human 51 | $ board-play.py t3 human 192.168.1.1 8000 # with ip addr and port 52 | 53 | To connect a client using one of the compatible `Monte Carlo Tree 54 | Search AI `_ players :: 55 | 56 | $ board-play.py t3 jrb.mcts.uct # number of wins metric 57 | $ board-play.py t3 jrb.mcts.uctv # point value of the board metric 58 | 59 | 60 | Games 61 | ----- 62 | 63 | Compatible games that have been implemented include: 64 | 65 | * `Reversi `_ 66 | * `Connect Four `_ 67 | * `Ultimate (or 9x9) Tic Tac Toe 68 | `_ 69 | * `Chong `_ 70 | -------------------------------------------------------------------------------- /boardserver/server.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function 3 | import json 4 | import random 5 | import sys 6 | 7 | import gevent, gevent.local, gevent.queue, gevent.server 8 | from six.moves import range 9 | 10 | 11 | class Server(object): 12 | def __init__(self, board, addr=None, port=None): 13 | self.board = board 14 | self.states = [] 15 | self.local = gevent.local.local() 16 | self.server = None 17 | # player message queues 18 | self.players = dict((x, gevent.queue.Queue()) 19 | for x in range(1, self.board.num_players+1)) 20 | # random player selection 21 | self.player_numbers = gevent.queue.JoinableQueue() 22 | 23 | self.addr = addr if addr is not None else '127.0.0.1' 24 | self.port = port if port is not None else 4242 25 | 26 | def game_reset(self): 27 | while True: 28 | # initialize the game state 29 | del self.states[:] 30 | state = self.board.starting_state() 31 | self.states.append(state) 32 | 33 | # update all players with the starting state 34 | state = self.board.to_json_state(state) 35 | # board = self.board.get_description() 36 | for x in range(1, self.board.num_players+1): 37 | self.players[x].put_nowait({ 38 | 'type': 'update', 39 | 'board': None, # board, 40 | 'state': state, 41 | }) 42 | 43 | # randomize the player selection 44 | players = list(range(1, self.board.num_players+1)) 45 | random.shuffle(players) 46 | for p in players: 47 | self.player_numbers.put_nowait(p) 48 | 49 | # block until all players have terminated 50 | self.player_numbers.join() 51 | 52 | def run(self): 53 | game = gevent.spawn(self.game_reset) 54 | self.server = gevent.server.StreamServer((self.addr, self.port), 55 | self.connection) 56 | print("Starting server...") 57 | self.server.serve_forever() 58 | 59 | # FIXME: need a way of nicely shutting down. 60 | # print "Stopping server..." 61 | # self.server.stop() 62 | 63 | def connection(self, socket, address): 64 | print("connection:", socket) 65 | self.local.socket = socket 66 | if self.player_numbers.empty(): 67 | self.send({ 68 | 'type': 'decline', 'message': "Game in progress." 69 | }) 70 | socket.close() 71 | return 72 | 73 | self.local.run = True 74 | self.local.player = self.player_numbers.get() 75 | self.send({'type': 'player', 'message': self.local.player}) 76 | 77 | while self.local.run: 78 | data = self.players[self.local.player].get() 79 | try: 80 | self.send(data) 81 | if data.get('winners') is not None: 82 | self.local.run = False 83 | 84 | elif data.get('state', {}).get('player') == self.local.player: 85 | message = '' 86 | while not message.endswith('\r\n'): 87 | message += socket.recv(4096).decode('utf-8') 88 | messages = message.rstrip().split('\r\n') 89 | self.parse(messages[0]) # FIXME: support for multiple messages 90 | # or out-of-band requests 91 | except Exception as e: 92 | print(e) 93 | socket.close() 94 | self.player_numbers.put_nowait(self.local.player) 95 | self.players[self.local.player].put_nowait(data) 96 | self.local.run = False 97 | self.player_numbers.task_done() 98 | 99 | def parse(self, msg): 100 | try: 101 | data = json.loads(msg) 102 | if data.get('type') != 'action': 103 | raise Exception 104 | self.handle_action(data) 105 | except Exception: 106 | self.players[self.local.player].put({ 107 | 'type': 'error', 'message': msg 108 | }) 109 | 110 | def handle_action(self, data): 111 | action = self.board.to_compact_action(data['message']) 112 | if not self.board.is_legal(self.states[-1], action): 113 | self.players[self.local.player].put({ 114 | 'type': 'illegal', 'message': data['message'], 115 | }) 116 | return 117 | 118 | self.states.append(self.board.next_state(self.states, action)) 119 | state = self.board.to_json_state(self.states[-1]) 120 | 121 | # TODO: provide a json object describing the board used 122 | data = { 123 | 'type': 'update', 124 | 'board': None, 125 | 'state': state, 126 | 'last_action': { 127 | 'player': self.board.previous_player(self.states[-1]), 128 | 'action': data['message'], 129 | 'sequence': len(self.states), 130 | }, 131 | } 132 | if self.board.is_ended(self.states[-1]): 133 | data['winners'] = self.board.win_values(self.states[-1]) 134 | data['points'] = self.board.points_values(self.states[-1]) 135 | 136 | for x in range(1, self.board.num_players+1): 137 | self.players[x].put(data) 138 | 139 | def send(self, data): 140 | self.local.socket.sendall("{0}\r\n".format(json.dumps(data)).encode('utf-8')) 141 | --------------------------------------------------------------------------------