├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── images ├── MchessAlpha.png ├── TurquoiseAlpha.png └── WebClientAlpha.png ├── mchess ├── .pylintrc ├── async_uci_agent.py ├── async_web_agent.py ├── chess_json_doc.md ├── chess_link.py ├── chess_link_agent.py ├── chess_link_bluepy.py ├── chess_link_protocol.py ├── chess_link_pyblue.py ├── chess_link_usb.py ├── engines │ └── engine-template.json ├── magic-board.md ├── requirements.txt ├── resources │ └── pieces │ │ ├── b.png │ │ ├── bb60.png │ │ ├── bb64.png │ │ ├── bk60.png │ │ ├── bk64.png │ │ ├── bn60.png │ │ ├── bn64.png │ │ ├── bp60.png │ │ ├── bp64.png │ │ ├── bq60.png │ │ ├── bq64.png │ │ ├── br60.png │ │ ├── br64.png │ │ ├── license.md │ │ ├── wb60.png │ │ ├── wb64.png │ │ ├── wk60.png │ │ ├── wk64.png │ │ ├── wn60.png │ │ ├── wn64.png │ │ ├── wp60.png │ │ ├── wp64.png │ │ ├── wq60.png │ │ ├── wq64.png │ │ ├── wr60.png │ │ └── wr64.png ├── terminal_agent.py ├── tk_agent.py ├── turquoise.py ├── turquoise_dispatch.py └── web │ ├── clean_inst.sh │ ├── favicon.ico │ ├── images │ ├── b.png │ ├── bb.png │ ├── bigmac.png │ ├── btn-bb.png │ ├── btn-bk.png │ ├── btn-bn.png │ ├── btn-bp.png │ ├── btn-bq.png │ ├── btn-br.png │ ├── btn-wb.png │ ├── btn-wk.png │ ├── btn-wn.png │ ├── btn-wp.png │ ├── btn-wq.png │ ├── btn-wr.png │ ├── f.png │ ├── ff.png │ ├── new_game.png │ ├── setup_position.png │ ├── stop.png │ ├── tree.png │ ├── turquoise.png │ └── wrench.png │ ├── index.html │ ├── package.json │ ├── scripts │ └── mchess.js │ └── styles │ └── mchess.css └── resources ├── chess_svg ├── Chess_bdt45.svg ├── Chess_blt45.svg ├── Chess_kdt45.svg ├── Chess_klt45.svg ├── Chess_ndt45.svg ├── Chess_nlt45.svg ├── Chess_pdt45.svg ├── Chess_plt45.svg ├── Chess_qdt45.svg ├── Chess_qlt45.svg ├── Chess_rdt45.svg ├── Chess_rlt45.svg ├── create_pngs.sh └── license.md └── turquoise.blend /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode/ 3 | .ipynb_checkpoints/ 4 | leelaz_opencl_tuning 5 | millennium_config.json 6 | uci_engines.json 7 | preferences.json 8 | chess_link_config.json 9 | mchess/engines/ 10 | mchess/web/node_modules 11 | mchess/mchess.log 12 | mchess/web/package-lock.json 13 | mchess/turquoise.log 14 | mchess/bin 15 | mchess/lib64 16 | mchess/lib 17 | mchess/include 18 | mchess/pyvenv.cfg 19 | .DS_Store 20 | \#* 21 | *~ 22 | .#* 23 | mchess/certs 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project includes third party modules that are subject to their 2 | own license. 3 | 4 | * Chess pieces (SVG) by [Cburnett](https://en.wikipedia.org/wiki/User:Cburnett) 5 | Licensed as: GFDL & BSD & GPL 6 | 7 | My own code: 8 | 9 | MIT License 10 | 11 | Copyright (c) 2018 Dominik Schlösser 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Mchess todos 2 | 3 | ## Bugs and testing 4 | 5 | - [ ] Handle inline TODOs 6 | 7 | ## Web 8 | 9 | - [ ] Game state console (save/load PGN, modes) 10 | - [ ] Log monitor 11 | - [ ] Debug console 12 | - [ ] Stats (Charts.js) 13 | - [ ] Manual analysis board 14 | - [ ] Moves clickable (variants, analysis-board) 15 | - [ ] select depth for analysis-boards (move-sequences?) 16 | - [ ] Turn board 17 | - [ ] Turn eboard 18 | - [ ] proper PWA boiler plate ('progressive web app') 19 | 20 | ## GUI 21 | 22 | - [ ] TkInter tests 23 | 24 | ## Agents and main 25 | 26 | - [ ] PGN-Libraries and ECO handling 27 | - [ ] Data import module 28 | 29 | ## Performance 30 | 31 | - [ ] General latency tests 32 | - [ ] ChessLink latency and general event-queue latency tests 33 | - [ ] Raspi event queue performance tests 34 | 35 | ## Game mode handling (and Web GUI) 36 | 37 | - [ ] Consistent game mode handling 38 | - [ ] Save PGN on exist and reload (fitting) history on restart 39 | - [ ] Dropboxes for oponents 40 | - [ ] Analysis buttons 41 | - [ ] State machine review 42 | 43 | ## Features and longer-term stuff (post first beta) 44 | 45 | - [ ] Local tournaments 46 | - [ ] more complex multi-agent topologies (e.g. two web agents with different players, 47 | remote connections between mchess instances, distributed tournaments) c.f. JSON-Prot. 48 | - [ ] MQTT agent 49 | - [ ] Mac/Windows bluetooth support for ChessLink 50 | - [ ] Elo calc 51 | - [ ] (PGN-)Library agent ['https://www.pgnmentor.com/files.html', 'http://caissabase.co.uk/' (scid), https://theweekinchess.com/index.php?q=twic] 52 | - [ ] ECO codes (https://github.com/hayatbiralem/eco.json/blob/master/eco.json) MIT license, (https://github.com/niklasf/eco) public domain 53 | - [ ] Lichess eval (e.g. https://github.com/cyanfish/python-lichess) 54 | - [ ] Checkout PyInstaller 55 | - [ ] Define proper agent JSON protocol (Net/JSON-UCI) 56 | - [ ] Unit-tests, Travis 57 | - [ ] PyPI publication 58 | 59 | ## Done 60 | 61 | ### ChessLink fixes and enhancements 62 | 63 | - [x] Javascript mess cleanup 64 | - [x] Move input via chess.js integration (solved without chess.js dependencies) 65 | 66 | - [x] Handle USB-reconnect 67 | - [x] Handle Bluetooth-reconnect 68 | - [x] Sync agent states (CL and others) to GUI-clients 69 | - [x] Select look-ahead engine for LED display 70 | - [x] Verify consistent mutex usage 71 | 72 | - [x] Current UCI API of python-chess is deprecated. Change to ASYNC engine api. 73 | - [x] Clear analysis of engines on new game or new pos 74 | - [x] Analyse python-chess corruptions (thread race-conditions?) 75 | - [x] Filter duplicate UCI depth messages (esp. lc0) 76 | - [x] Bluetooth LE sometimes fails to connect. Retry strategy does not work (other than restarting) 77 | - [x] Better handling of display of check mate situations with Millennium Board (led king animation?) 78 | -------------------------------------------------------------------------------- /images/MchessAlpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/images/MchessAlpha.png -------------------------------------------------------------------------------- /images/TurquoiseAlpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/images/TurquoiseAlpha.png -------------------------------------------------------------------------------- /images/WebClientAlpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/images/WebClientAlpha.png -------------------------------------------------------------------------------- /mchess/.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | extension-pkg-whitelist=PyQt5 3 | 4 | [MESSAGES CONTROL] 5 | logging-format-style=fstr 6 | 7 | [LOGGING] 8 | max-line-length=120 9 | -------------------------------------------------------------------------------- /mchess/async_uci_agent.py: -------------------------------------------------------------------------------- 1 | ''' Chess UCI Engine agent using python-chess's async interface ''' 2 | import logging 3 | import time 4 | import queue 5 | import json 6 | import os 7 | import threading 8 | import copy 9 | from distutils.spawn import find_executable 10 | import glob 11 | 12 | import asyncio 13 | import chess 14 | import chess.engine 15 | 16 | 17 | class UciEngines: 18 | """Search for UCI engines and make a list of all available engines 19 | """ 20 | ENGINE_JSON_VERSION = 1 21 | 22 | def __init__(self, appque, prefs): 23 | self.log = logging.getLogger("UciEngines") 24 | self.prefs = prefs 25 | self.appque = appque 26 | self.name = "UciEngines" 27 | 28 | COMMON_ENGINES = ['stockfish', 'crafty', 'komodo'] 29 | for engine_name in COMMON_ENGINES: 30 | engine_json_path = os.path.join('engines', engine_name + '.json') 31 | if os.path.exists(engine_json_path): 32 | inv = False 33 | try: 34 | with open(engine_json_path) as f: 35 | engine_json = json.load(f) 36 | if 'version' in engine_json and \ 37 | engine_json['version'] == self.ENGINE_JSON_VERSION: 38 | inv = False 39 | else: 40 | self.log.warning( 41 | f"Wrong version information in {engine_json_path}") 42 | inv = True 43 | except Exception as e: 44 | self.log.error( 45 | f"Json engine load of {engine_json_path} failed: {e}") 46 | inv = True 47 | if inv is False: 48 | continue 49 | engine_path = find_executable(engine_name) 50 | if engine_path is not None: 51 | engine_json = {'name': engine_name, 52 | 'path': engine_path, 53 | 'active': True, 54 | 'version': self.ENGINE_JSON_VERSION} 55 | with open(engine_json_path, 'w') as f: 56 | try: 57 | json.dump(engine_json, f, indent=4) 58 | except Exception as e: 59 | self.log.error( 60 | f'Failed to write no engine description {engine_json_path}: {e}') 61 | continue 62 | self.log.info(f'Found new/updated UCI engine {engine_name}') 63 | self.engine_json_list = glob.glob('engines/*.json') 64 | if len(self.engine_json_list) == 0: 65 | self.log.warning( 66 | 'No UCI engines found, and none is defined in engines subdir.') 67 | self.engines = {} 68 | for engine_json_path in self.engine_json_list: 69 | if '-template' in engine_json_path or '-help' in engine_json_path: 70 | continue 71 | try: 72 | with open(engine_json_path, 'r') as f: 73 | engine_json = json.load(f) 74 | except Exception as e: 75 | self.log.error( 76 | f'Failed to read UCI engine description {engine_json_path}: {e}') 77 | continue 78 | if 'name' not in engine_json: 79 | self.log.error("Mandatory parameter 'name' is not in UCI description " 80 | f"{engine_json_path}, ignoring this engine.") 81 | continue 82 | if 'path' not in engine_json: 83 | self.log.error("Mandatory parameter 'path' is not in UCI description " 84 | f"{engine_json_path}, ignoring this engine.") 85 | continue 86 | if os.path.exists(engine_json['path']) is False: 87 | self.log.error("Invalid path {engine_json['path']} in UCI description " 88 | f"{engine_json_path}, ignoring this engine.") 89 | continue 90 | 91 | if 'active' not in engine_json or engine_json['active'] is False: 92 | self.log.debug(f"UCI engine at {engine_json_path} has not property " 93 | "'active': true, ignoring this engine.") 94 | continue 95 | 96 | base_name, _ = os.path.splitext(engine_json_path) 97 | engine_json_help_path = base_name + "-help.json" 98 | engine_json['help_path'] = engine_json_help_path 99 | engine_json['json_path'] = engine_json_path 100 | name = engine_json['name'] 101 | self.engines[name] = {} 102 | self.engines[name]['params'] = engine_json 103 | self.log.debug(f"{len(self.engines)} engine descriptions loaded.") 104 | # self.publish_uci_engines() 105 | 106 | def publish_uci_engines(self): 107 | uci_standard_options = ["Threads", "MultiPV", "SyzygyPath", "Ponder", 108 | "UCI_Elo", "Hash"] 109 | engine_list = {} 110 | for engine in self.engines: 111 | engine_list[engine] = {} 112 | engine_list[engine] = { 113 | "name": self.engines[engine]["params"]["name"], 114 | "active": self.engines[engine]["params"]["active"], 115 | "options": {} 116 | } 117 | for opt in uci_standard_options: 118 | if "uci-options" in self.engines[engine]["params"]: 119 | if opt in self.engines[engine]["params"]["uci-options"]: 120 | engine_list[engine]["options"][opt] = self.engines[engine]["params"]["uci-options"][opt] 121 | self.appque.put({ 122 | "cmd": "engine_list", 123 | "actor": self.name, 124 | "engines": engine_list 125 | }) 126 | 127 | 128 | class UciAgent: 129 | """ Support for single UCI chess engine """ 130 | 131 | def __init__(self, appque, engine_json, prefs): 132 | self.active = False 133 | self.que = appque 134 | self.engine_json = engine_json 135 | self.prefs = prefs 136 | self.name = engine_json['name'] 137 | self.log = logging.getLogger('UciAgent_' + self.name) 138 | # self.engine = engine_spec['engine'] 139 | # self.ponder_board = None 140 | self.active = True 141 | self.busy = False 142 | self.thinking = False 143 | self.stopping = False 144 | # Asyncio queues are not thread-safe, hence useless here. 145 | self.cmd_que = queue.Queue() 146 | self.thinking = False 147 | self.analysisresults = None 148 | # self.loop=asyncio.new_event_loop() 149 | self.worker = threading.Thread(target=self.async_agent_thread, args=()) 150 | self.worker.setDaemon(True) 151 | self.worker.start() 152 | self.info_throttle = 0.5 153 | self.version_name = self.name + " 1.0" 154 | self.authors = "" 155 | self.engine = None 156 | self.transport = None 157 | self.loop_active = False 158 | 159 | async def async_quit(self): 160 | try: 161 | await self.engine.quit() 162 | except Exception as _: 163 | del _ 164 | # Something has changed with timing in Python 3.9, ignore quit-error. 165 | pass 166 | 167 | def quit(self): 168 | # ft = self.engine.terminate(async_callback=True) 169 | # ft.result() 170 | asyncio.run(self.async_quit()) 171 | self.active = False 172 | 173 | def agent_ready(self): 174 | return self.active 175 | 176 | def send_agent_state(self, state, msg=""): 177 | stmsg = {'cmd': 'agent_state', 'state': state, 'message': msg, 'name': self.version_name, 178 | 'authors': self.authors, 'class': 'engine', 'actor': self.name} 179 | self.que.put(stmsg) 180 | self.log.debug(f"Sent {stmsg}") 181 | 182 | async def uci_open_engine(self): 183 | try: 184 | if 'engine_params' in self.engine_json: 185 | engine_path = [self.engine_json['path']] + self.engine_json['engine_params'] 186 | else: 187 | engine_path = self.engine_json['path'] 188 | transport, engine = await chess.engine.popen_uci(engine_path) 189 | self.engine = engine 190 | self.transport = transport 191 | self.log.info(f"Engine {self.name} opened.") 192 | try: 193 | if 'name' in self.engine.id: 194 | self.version_name = self.engine.id['name'] 195 | if 'author' in self.engine.id: 196 | self.authors = self.engine.id['author'] 197 | except Exception as e: 198 | self.log.error( 199 | f"Failed to get engine-id-info {self.engine.id}: {e}") 200 | self.log.debug(f"Engine id: {self.engine.id}") 201 | except Exception as e: 202 | self.log.error(f"Failed to popen UCI engine {self.name} at " 203 | f"{self.engine_json['path']}, ignoring: {e}") 204 | self.engine = None 205 | self.transport = None 206 | return False 207 | 208 | optsh = {} 209 | opts = {} 210 | rewrite_json = False 211 | if os.path.exists(self.engine_json['json_path']) is False: 212 | rewrite_json = True 213 | self.engine_json['uci-options'] = {} 214 | if 'version' not in self.engine_json or \ 215 | self.engine_json['version'] < UciEngines.ENGINE_JSON_VERSION: 216 | self.log.error( 217 | f"{self.engine_json['json_path']} is outdated. Resetting content") 218 | rewrite_json = True 219 | self.engine_json['version'] = UciEngines.ENGINE_JSON_VERSION 220 | if 'uci-options' not in self.engine_json or self.engine_json['uci-options'] == {}: 221 | rewrite_json = True 222 | self.engine_json['uci-options'] = {} 223 | else: 224 | for opt in self.engine.options: 225 | if opt not in self.engine_json['uci-options']: 226 | entries = self.engine.options[opt] 227 | # Ignore buttons 228 | if entries.type != 'button': 229 | self.log.warning( 230 | f'New UCI opt {opt} for {self.name}, reset to defaults') 231 | rewrite_json = True 232 | 233 | if rewrite_json is True: 234 | self.log.info( 235 | f"Writing defaults for {self.name} to {self.engine_json['json_path']}") 236 | for opt in self.engine.options: 237 | entries = self.engine.options[opt] 238 | optvs = {} 239 | optvs['name'] = entries.name 240 | optvs['type'] = entries.type 241 | optvs['default'] = entries.default 242 | optvs['min'] = entries.min 243 | optvs['max'] = entries.max 244 | optvs['var'] = entries.var 245 | optsh[opt] = optvs 246 | # TODO: setting buttons to their default causes python_chess uci 247 | # to crash (komodo 9), see above 248 | if entries.type != 'button': 249 | opts[opt] = entries.default 250 | self.engine_json['uci-options'] = opts 251 | self.engine_json['uci-options-help'] = optsh 252 | try: 253 | with open(self.engine_json['json_path'], 'w') as f: 254 | json.dump(self.engine_json, f, indent=4) 255 | except Exception as e: 256 | self.log.error( 257 | f"Can't save engine.json to {self.engine_json['json_path']}, {e}") 258 | try: 259 | with open(self.engine_json['help_path'], 'w') as f: 260 | json.dump( 261 | self.engine_json['uci-options-help'], f, indent=4) 262 | except Exception as e: 263 | self.log.error( 264 | f"Can't save help to {self.engine_json['help_path']}, {e}") 265 | else: 266 | opts = self.engine_json['uci-options'] 267 | 268 | auto_opts = ['Ponder', 'MultiPV', 'UCI_Chess960'] 269 | def_opts = copy.deepcopy(opts) 270 | for op in auto_opts: 271 | if op in def_opts: 272 | del def_opts[op] 273 | 274 | await self.engine.configure(def_opts) 275 | self.log.debug(f"Ping {self.name}") 276 | await self.engine.ping() 277 | self.log.debug(f"Pong {self.name}") 278 | self.send_agent_state('idle') 279 | return True 280 | 281 | async def async_stop(self): 282 | if self.stopping is True: 283 | self.log.warning('Stop aready in progress.') 284 | return 285 | if self.thinking is True: 286 | self.log.info('Initiating async stop') 287 | self.stopping = True 288 | if self.analysisresults is not None: 289 | self.analysisresults.stop() 290 | 291 | async def async_go(self, board, mtime, ponder=False, analysis=False): 292 | if mtime != -1: 293 | mtime = mtime / 1000.0 294 | if ponder is True: 295 | self.log.warning("Ponder not implemented!") 296 | pv = [] 297 | last_info = [] 298 | self.log.debug(f"mtime: {mtime}") 299 | if 'MultiPV' in self.engine_json['uci-options']: 300 | mpv = self.engine_json['uci-options']['MultiPV'] 301 | for i in range(mpv): 302 | pv.append([]) 303 | last_info.append(0) 304 | res = {'cmd': 'current_move_info', 305 | 'multipv_index': i + 1, 306 | 'variant': [], 307 | 'actor': self.name, 308 | 'score': '' 309 | } 310 | self.que.put(res) # reset old evals 311 | else: 312 | pv.append([]) 313 | mpv = 1 314 | if mtime == -1: 315 | self.log.debug("Infinite analysis") 316 | lm = None 317 | else: 318 | lm = chess.engine.Limit(time=mtime) 319 | rep = None 320 | skipped = False 321 | self.send_agent_state('busy') 322 | self.log.info(f"Starting UCI {self.name}") 323 | info = None 324 | best_score = None 325 | with await self.engine.analysis(board, lm, multipv=mpv, info=chess.engine.Info.ALL) \ 326 | as self.analysisresults: 327 | async for info in self.analysisresults: 328 | if self.stopping is True: 329 | self.log.info("Stop: request, aborting calc.") 330 | break 331 | self.log.debug(info) 332 | if 'pv' in info: 333 | if 'multipv' in info: 334 | ind = info['multipv'] - 1 335 | else: 336 | ind = 0 337 | pv[ind] = [] 338 | for mv in info['pv']: 339 | pv[ind].append(mv.uci()) 340 | rep = {'cmd': 'current_move_info', 341 | 'multipv_index': ind + 1, 342 | 'variant': pv[ind], 343 | 'actor': self.name 344 | } 345 | if 'score' in info: 346 | try: 347 | # if info['score'].is_mate(): 348 | # sc = str(info['score']) 349 | # else: 350 | # cp = float(str(info['score']))/100.0 351 | # sc = '{:.2f}'.format(cp) 352 | if info['score'].is_mate(): 353 | sc = f"#{info['score'].relative.mate()}" 354 | else: 355 | sc = info['score'].relative.score(mate_score=10000) / 100.0 356 | except Exception as e: 357 | self.log.error( 358 | f"Score transform failed {info['score']}: {e}") 359 | sc = '?' 360 | rep['score'] = sc 361 | if ind == 0: 362 | best_score = sc 363 | if 'depth' in info: 364 | rep['depth'] = info['depth'] 365 | if 'seldepth' in info: 366 | rep['seldepth'] = info['seldepth'] 367 | if 'nps' in info: 368 | rep['nps'] = info['nps'] 369 | if 'tbhits' in info: 370 | rep['tbhits'] = info['tbhits'] 371 | if time.time() - last_info[ind] > self.info_throttle: 372 | self.que.put(rep) 373 | last_info[ind] = time.time() 374 | skipped = False 375 | else: 376 | skipped = True 377 | 378 | self.analysisresults = None 379 | self.log.debug("thinking comes to end") 380 | if skipped is True and rep is not None: 381 | self.que.put(rep) 382 | rep = None 383 | if len(pv) > 0 and len(pv[0]) > 0: 384 | if analysis is False: 385 | move = pv[0][0] 386 | rep = {'cmd': 'move', 387 | 'uci': move, 388 | 'actor': self.name 389 | } 390 | if best_score is not None: 391 | rep['score'] = best_score 392 | if 'depth' in info: 393 | rep['depth'] = info['depth'] 394 | if 'seldepth' in info: 395 | rep['seldepth'] = info['seldepth'] 396 | if 'nps' in info: 397 | rep['nps'] = info['nps'] 398 | if 'tbhits' in info: 399 | rep['tbhits'] = info['tbhits'] 400 | 401 | self.log.debug(f"Queing result: {rep}") 402 | self.que.put(rep) 403 | self.log.info('Calc finished.') 404 | else: 405 | self.log.error('Engine returned no move.') 406 | self.thinking = False 407 | self.stopping = False 408 | self.send_agent_state('idle') 409 | 410 | def stop(self): 411 | self.log.info('synchr stop received') 412 | if self.thinking is False: 413 | self.log.debug(f"No need to stop {self.name}, not running.") 414 | asyncio.run(self.async_stop()) 415 | 416 | def go(self, board, mtime, ponder=False, analysis=False): 417 | self.log.info('go received') 418 | if self.thinking is True: 419 | self.log.error( 420 | f"Can't start engine {self.name}: it's already busy!") 421 | return False 422 | self.thinking = True 423 | self.stopping = False 424 | cmd = {'board': board, 'mtime': mtime, 425 | 'ponder': ponder, 'analysis': analysis} 426 | self.cmd_que.put(cmd) 427 | return True 428 | 429 | async def uci_event_loop(self): 430 | ok = await self.uci_open_engine() 431 | self.loop_active = True 432 | if ok is True: 433 | while self.loop_active is True: 434 | try: 435 | cmd = self.cmd_que.get_nowait() 436 | self.log.debug("Go!") 437 | await self.async_go(cmd['board'], cmd['mtime'], ponder=cmd['ponder'], 438 | analysis=cmd['analysis']) 439 | self.cmd_que.task_done() 440 | except queue.Empty: 441 | await asyncio.sleep(0.05) 442 | except Exception as e: 443 | self.log.warning(f"Failed to get que: {e}") 444 | 445 | def async_agent_thread(self): 446 | asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy()) 447 | asyncio.run(self.uci_event_loop()) 448 | -------------------------------------------------------------------------------- /mchess/async_web_agent.py: -------------------------------------------------------------------------------- 1 | ''' Web interface using aiohttp ''' 2 | import logging 3 | import json 4 | import threading 5 | import asyncio 6 | import ssl 7 | import aiohttp 8 | from aiohttp import web 9 | import chess 10 | import chess.pgn 11 | import time 12 | import os 13 | 14 | 15 | class AsyncWebAgent: 16 | def __init__(self, appque, prefs): 17 | self.name = 'AsyncWebAgent' 18 | self.prefs = prefs 19 | self.log = logging.getLogger("AsyncWebAgent") 20 | self.log.setLevel(logging.INFO) 21 | self.appque = appque 22 | self.orientation = True 23 | self.active = False 24 | self.max_plies = 6 25 | 26 | self.display_cache = "" 27 | self.last_cursor_up = 0 28 | self.move_cache = "" 29 | self.info_cache = "" 30 | self.info_provider = {} 31 | self.agent_state_cache = {} 32 | self.uci_engines_cache = {} 33 | self.display_move_cache = {} 34 | self.valid_moves_cache = {} 35 | self.game_stats_cache = {} 36 | self.max_mpv = 1 37 | self.last_board = None 38 | self.last_attribs = None 39 | self.last_pgn = None 40 | self.socket_thread_active = False 41 | self.ws_clients = [] 42 | 43 | if 'port' in self.prefs: 44 | self.port = self.prefs['port'] 45 | else: 46 | self.port = 8001 47 | self.log.warning(f'Port not configured, defaulting to {self.port}') 48 | 49 | if 'bind_address' in self.prefs: 50 | if isinstance(self.prefs['bind_address'], list) is True: 51 | self.bind_addresses = self.prefs['bind_address'] 52 | else: 53 | self.bind_addresses = self.prefs['bind_address'] 54 | else: 55 | self.bind_addresses = 'localhost' 56 | self.log.warning( 57 | f'Bind_address not configured, defaulting to {self.bind_address}, set to "[\'0.0.0.0\']" for remote accessibility' 58 | ) 59 | 60 | self.private_key = None 61 | self.public_key = None 62 | self.tls = False 63 | if 'tls' in self.prefs and self.prefs['tls'] is True: 64 | if 'private_key' not in self.prefs or 'public_key' not in self.prefs: 65 | self.log.error("Cannot configure tls without public_key and private_key configured!") 66 | self.log.warning("Downgraded to tls=False") 67 | else: 68 | if os.path.exists(self.prefs['private_key']) is False: 69 | self.log.error(f"Private key file {self.prefs['private_key']} does not exist, downgrading to no TLS") 70 | elif os.path.exists(self.prefs['public_key']) is False: 71 | self.log.error(f"Public key file {self.prefs['public_key']} does not exist, downgrading to no TLS") 72 | else: 73 | self.private_key = self.prefs['private_key'] 74 | self.public_key = self.prefs['public_key'] 75 | self.tls = True 76 | 77 | self.figrep = {"int": [1, 2, 3, 4, 5, 6, 0, -1, -2, -3, -4, -5, -6], 78 | "pythc": [(chess.PAWN, chess.WHITE), (chess.KNIGHT, chess.WHITE), (chess.BISHOP, chess.WHITE), 79 | (chess.ROOK, chess.WHITE), (chess.QUEEN, chess.WHITE), (chess.KING, chess.WHITE), 80 | (chess.PAWN, chess.BLACK), (chess.KNIGHT, chess.BLACK), (chess.BISHOP, chess.BLACK), 81 | (chess.ROOK, chess.BLACK), (chess.QUEEN, chess.BLACK), (chess.KING, chess.BLACK)], 82 | "unic": "♟♞♝♜♛♚ ♙♘♗♖♕♔", 83 | "ascii": "PNBRQK.pnbrqk"} 84 | self.chesssym = {"unic": ["-", "×", "†", "‡", "½"], 85 | "ascii": ["-", "x", "+", "#", "1/2"]} 86 | 87 | self.event_loop = asyncio.new_event_loop() 88 | asyncio.set_event_loop(self.event_loop) 89 | self.worker = threading.Thread(target=self.web_agent_thread, args=()) 90 | self.worker.setDaemon(True) 91 | self.worker.start() 92 | self.active = True 93 | 94 | def web_agent_thread(self): 95 | self.socket_thread_active = True 96 | asyncio.set_event_loop(self.event_loop) 97 | self.app = web.Application(debug=True) 98 | self.app.add_routes([web.static('/node_modules', 'web/node_modules'), 99 | web.static('/images', 'web/images')]) 100 | 101 | self.app.add_routes([web.get('/', self.web_root), 102 | web.get('/favicon.ico', self.web_favicon), 103 | web.get('/scripts/mchess.js', self.mchess_script), 104 | web.get('/styles/mchess.css', self.mchess_style)]) 105 | self.app.add_routes([web.get('/ws', self.websocket_handler)]) 106 | if self.tls is True: 107 | self.ssl_context = ssl.SSLContext() # = TLS 108 | try: 109 | self.ssl_context.load_cert_chain(self.public_key, self.private_key) 110 | except Exception as e: 111 | self.log.error(f"Cannot create cert chain: {e}, not using TLS") 112 | self.tls = False 113 | asyncio.run(self.async_web_agent()) 114 | while self.active is True: 115 | time.sleep(0.1) 116 | self.log.info("Web starter thread stopped") 117 | 118 | async def async_web_agent(self): 119 | runner = web.AppRunner(self.app) 120 | await runner.setup() 121 | self.log.info("Starting web runner") 122 | if self.tls is True: 123 | self.log.info(f"TLS active, bind={self.bind_addresses}, port={self.port}") 124 | print(f"Webclient at: https://localhost:{self.port}") 125 | site = web.TCPSite(runner, self.bind_addresses, self.port, ssl_context=self.ssl_context) 126 | else: 127 | self.log.info(f"TLS NOT active, bind={self.bind_addresses}, port={self.port}") 128 | site = web.TCPSite(runner, self.bind_addresses, self.port) 129 | print(f"Webclient at: http://localhost:{self.port}") 130 | await site.start() 131 | self.log.info("Web server active") 132 | while self.active: 133 | await asyncio.sleep(0.1) 134 | self.log.info("Web server stopped") 135 | 136 | def web_root(self, request): 137 | return web.FileResponse('web/index.html') 138 | 139 | def web_favicon(self, request): 140 | return web.FileResponse('web/favicon.ico') 141 | 142 | def mchess_script(self, request): 143 | return web.FileResponse('web/scripts/mchess.js') 144 | 145 | def mchess_style(self, request): 146 | return web.FileResponse('web/styles/mchess.css') 147 | 148 | async def send_out(self, ws, text): 149 | await ws.send_str(text) 150 | 151 | def send2ws(self, ws, text): 152 | if ws.closed: 153 | self.log.warning(f"Closed websocket encountered: {ws}") 154 | return False 155 | self.log.info(f"Sending {text} to {ws}") 156 | asyncio.run(self.send_out(ws, text)) 157 | return True 158 | 159 | async def websocket_handler(self, request): 160 | ws = web.WebSocketResponse() 161 | thread_log = logging.getLogger("ThrdWeb") 162 | thread_log.setLevel(logging.INFO) 163 | 164 | await ws.prepare(request) 165 | 166 | if ws not in self.ws_clients: 167 | self.ws_clients.append(ws) 168 | thread_log.info(f"New ws client {ws}! (clients: {len(self.ws_clients)})") 169 | else: 170 | thread_log.info(f"Client already registered! (clients: {len(self.ws_clients)})") 171 | 172 | if self.last_board is not None and self.last_attribs is not None: 173 | msg = {'cmd': 'display_board', 'fen': self.last_board.fen(), 'pgn': self.last_pgn, 174 | 'attribs': self.last_attribs} 175 | try: 176 | await ws.send_str(json.dumps(msg)) 177 | except Exception as e: 178 | thread_log.warning( 179 | "Sending to WebSocket client {} failed with {}".format(ws, e)) 180 | return 181 | for actor in self.agent_state_cache: 182 | msg = self.agent_state_cache[actor] 183 | try: 184 | await ws.send_str(json.dumps(msg)) 185 | except Exception as e: 186 | thread_log.warning( 187 | f"Failed to update agents states to new web-socket client: {e}") 188 | if self.uci_engines_cache != {}: 189 | await ws.send_str(json.dumps(self.uci_engines_cache)) 190 | if self.display_move_cache != {}: 191 | await ws.send_str(json.dumps(self.display_move_cache)) 192 | if self.valid_moves_cache != {}: 193 | await ws.send_str(json.dumps(self.valid_moves_cache)) 194 | if self.game_stats_cache != {}: 195 | await ws.send_str(json.dumps(self.game_stats_cache)) 196 | 197 | async for msg in ws: 198 | if msg.type == aiohttp.WSMsgType.TEXT: 199 | # if msg.data is not None: 200 | thread_log.info( 201 | "Client ws_dispatch: ws:{} msg:{}".format(ws, msg.data)) 202 | try: 203 | self.log.info(f"Received: {msg.data}") 204 | self.appque.put(json.loads(msg.data)) 205 | except Exception as e: 206 | thread_log.warning(f"WebClient sent invalid JSON: {msg.data}: {e}") 207 | # if msg.data == 'close': 208 | # await ws.close() 209 | # else: 210 | # await ws.send_str(msg.data + '/answer') 211 | elif msg.type == aiohttp.WSMsgType.ERROR: 212 | thread_log.warning(f'ws connection closed with exception {ws.exception()}') 213 | break 214 | else: 215 | thread_log.error(f"Unexpected message {msg.data}, of type {msg.type}") 216 | break 217 | thread_log.warning(f"WS-CLOSE: {ws}") 218 | self.ws_clients.remove(ws) 219 | 220 | return ws 221 | 222 | def agent_ready(self): 223 | return self.active 224 | 225 | def quit(self): 226 | self.socket_thread_active = False 227 | 228 | def display_board(self, board, attribs={'unicode': True, 'invert': False, 'white_name': 'white', 'black_name': 'black'}): 229 | self.last_board = board 230 | self.last_attribs = attribs 231 | self.log.info(f"Display board, clients: {len(self.ws_clients)}") 232 | try: 233 | game = chess.pgn.Game().from_board(board) 234 | game.headers["White"] = attribs["white_name"] 235 | game.headers["Black"] = attribs["black_name"] 236 | pgntxt = str(game) 237 | except Exception as e: 238 | self.log.error("Invalid PGN position, {}".format(e)) 239 | return 240 | self.last_pgn = pgntxt 241 | # print("pgn: {}".format(pgntxt)) 242 | msg = {'cmd': 'display_board', 'fen': board.fen(), 'pgn': pgntxt, 243 | 'attribs': attribs} 244 | for ws in self.ws_clients: 245 | try: 246 | if self.send2ws(ws, json.dumps(msg)) is False: 247 | self.ws_clients.remove(ws) 248 | except Exception as e: 249 | self.log.warning( 250 | "Sending board to WebSocket client {} failed with {}".format(ws, e)) 251 | 252 | def display_move(self, move_msg): 253 | self.log.info(f"AWS display move to {len(self.ws_clients)} clients") 254 | self.display_move_cache = move_msg 255 | for ws in self.ws_clients: 256 | try: 257 | if self.send2ws(ws, json.dumps(move_msg)) is False: 258 | self.ws_clients.remove(ws) 259 | except Exception as e: 260 | self.log.warning( 261 | f"Sending display_move {move_msg} to WebSocket client {ws} failed with {e}") 262 | 263 | def set_valid_moves(self, board, vals): 264 | self.log.info(f"web set valid called, clients: {len(self.ws_clients)}.") 265 | self.valid_moves_cache = { 266 | "cmd": "valid_moves", 267 | "valid_moves": [], 268 | 'actor': 'AsyncWebAgent' 269 | } 270 | if vals is not None: 271 | for v in vals: 272 | self.valid_moves_cache['valid_moves'].append(vals[v]) 273 | self.log.info(f"Valid-moves: {self.valid_moves_cache}") 274 | for ws in self.ws_clients: 275 | try: 276 | if self.send2ws(ws, json.dumps(self.valid_moves_cache)) is False: 277 | self.ws_clients.remove(ws) 278 | except Exception as e: 279 | self.log.warning( 280 | "Sending valid_moves to WebSocket client {} failed with {}".format(ws, e)) 281 | 282 | def display_info(self, board, info): 283 | for ws in self.ws_clients: 284 | try: 285 | if self.send2ws(ws, json.dumps(info)) is False: 286 | self.ws_clients.remove(ws) 287 | except Exception as e: 288 | self.log.warning( 289 | "Sending display-info to WebSocket client {} failed with {}".format(ws, e)) 290 | 291 | def engine_list(self, msg): 292 | for engine in msg["engines"]: 293 | self.log.info(f"Engine {engine} announced.") 294 | self.uci_engines_cache = msg 295 | for ws in self.ws_clients: 296 | try: 297 | if self.send2ws(ws, json.dumps(msg)) is False: 298 | self.ws_clients.remove(ws) 299 | except Exception as e: 300 | self.log.warning( 301 | "Sending uci-info to WebSocket client {} failed with {}".format(ws, e)) 302 | 303 | def game_stats(self, stats): 304 | msg = {'cmd': 'game_stats', 'stats': stats, 'actor': 'AsyncWebAgent'} 305 | self.game_stats_cache = msg 306 | self.log.info(f"Game stats: {msg}") 307 | for ws in self.ws_clients: 308 | try: 309 | if self.send2ws(ws, json.dumps(msg)) is False: 310 | self.ws_clients.remove(ws) 311 | except Exception as e: 312 | self.log.warning( 313 | "Sending game_stats to WebSocket client {} failed with {}".format(ws, e)) 314 | 315 | def agent_states(self, msg): 316 | self.agent_state_cache[msg['actor']] = msg 317 | for ws in self.ws_clients: 318 | try: 319 | if self.send2ws(ws, json.dumps(msg)) is False: 320 | self.ws_clients.remove(ws) 321 | except Exception as e: 322 | self.log.warning( 323 | "Sending agent-state info to WebSocket client {} failed with {}".format(ws, e)) 324 | -------------------------------------------------------------------------------- /mchess/chess_json_doc.md: -------------------------------------------------------------------------------- 1 | # Json commands for chess agent interaction 2 | 3 | - Revision 0.1.0, 2020-June-18 4 | 5 | This JSON protocol is used for agents communicating with the dispatcher 6 | and for network-connections (e.g. websocket clients). 7 | 8 | ## Game modes 9 | 10 | ### New game 11 | 12 | ```json 13 | { 14 | "cmd": "new_game", 15 | "mode": "optional-game-mode", 16 | "actor": "name-of-agent-sending-this" 17 | } 18 | ``` 19 | 20 | ### Game-mode (Human/computer vs human/computer) 21 | 22 | ```json 23 | { 24 | "cmd": "game_mode", 25 | "mode": "human-human or human-computer or computer-human or computer_computer. computer can optionally be an engine-name", 26 | "level": "currently: optional computer think-time in ms", 27 | "actor": "name-of-agent-sending-this" 28 | } 29 | ``` 30 | 31 | ### Set computer play-strength level 32 | 33 | ```json 34 | { 35 | "cmd": "set_level", 36 | "level": "currently: computer think-time in ms", 37 | "actor": "name-of-agent-sending-this" 38 | } 39 | ``` 40 | 41 | ### Quit, end program 42 | 43 | ```json 44 | { 45 | "cmd": "quit", 46 | "actor": "name-of-agent-sending-this" 47 | } 48 | ``` 49 | 50 | ### Start analysis with chess engine 51 | 52 | ```json 53 | { 54 | "cmd": "analyse", 55 | "actor": "name-of-agent-sending-this" 56 | } 57 | ``` 58 | 59 | ### Stop engine 60 | 61 | ```json 62 | { 63 | "cmd": "stop", 64 | "actor": "name-of-agent-sending-this" 65 | } 66 | ``` 67 | 68 | ### Start engine (go) 69 | 70 | ```json 71 | { 72 | "cmd": "go", 73 | "actor": "name-of-agent-sending-this" 74 | } 75 | ``` 76 | 77 | ### Turn (select side to move next, insert zero move, if necessary) 78 | 79 | ```json 80 | { 81 | "cmd": "turn", 82 | "color": "white or black", 83 | "actor": "name-of-agent-sending-this" 84 | } 85 | ``` 86 | 87 | ### Import FEN position 88 | 89 | ```json 90 | { 91 | "cmd": "import_fen", 92 | "fen": "FEN-encoded-position", 93 | "actor": "name-of-agent-sending-this" 94 | } 95 | ``` 96 | 97 | ### Import PGN game 98 | 99 | ```json 100 | { 101 | "cmd": "import_pgn", 102 | "pgn": "pgn-text", 103 | "actor": "name-of-agent-sending-this" 104 | } 105 | ``` 106 | 107 | ## Game state information received by agents 108 | 109 | ### Update board display 110 | 111 | This message is sent, if the board position changes. (New move, new game, 112 | position imported etc.) 113 | 114 | ```json 115 | { 116 | "cmd": "display_board", 117 | "fen": "FEN position", 118 | "pgn": "PGN game history", 119 | "attribs": { 120 | "unicode": true, 121 | "invert": false, 122 | "white": "name-of-white-player", 123 | "black": "name-of-black-player" 124 | } 125 | } 126 | ``` 127 | 128 | ### Engine information 129 | 130 | Provide information while UCI chess computer engine calculates about 131 | best variations and evaluations. This message is sent often. 132 | 133 | ```json 134 | { 135 | "cmd": "current_move_info", 136 | "multipv_index": "index of variant: 1 is main variant", 137 | "score": "centi-pawn score or #2 mate announcement", 138 | "depth": "search depth (half moves)", 139 | "seldepth": "selective search depth (half moves)", 140 | "nps": "nodes per second", 141 | "tbhits": "table-base hits", 142 | "variant": [ 143 | ["half-move-number", "uci-formatted moves"], 144 | ["half-move-number", "uci-formatted moves"] 145 | ], 146 | "san_variant": [ 147 | ["full-move-number", "white-move or ..", "black-move"], 148 | ["full-move-number", "white-move", "black-move or empty"] 149 | ], 150 | "preview_fen_depth": "number of half moves for preview FEN", 151 | "preview_fen": "FEN half-moves in the future", 152 | "actor": "name-of-agent-sending-this" 153 | } 154 | ``` 155 | 156 | The generator should provide only `"variant"` in uci format, a san-formatted variantformat 157 | is added by the dispatcher for client-display use. 158 | 159 | ### Game stats 160 | 161 | Provide information about eval and resource stats. 162 | 163 | ```json 164 | { 165 | "cmd": "game_stats", 166 | "stats": [ 167 | { 168 | "score": "centi-pawn score or #2 mate announcement", 169 | "depth": "search depth (half moves)", 170 | "seldepth": "selective search depth (half moves)", 171 | "nps": "nodes per second", 172 | "tbhits": "table-base hits", 173 | "move_number": "full move number", 174 | "halfmove_number: "half-move-number", 175 | "color": "WHITE or BLACK", 176 | "player": "playername" 177 | }, 178 | ``` 179 | 180 | ... 181 | 182 | ```json 183 | ], 184 | "actor": "name-of-agent-sending-this" 185 | } 186 | ``` 187 | 188 | The generator should provide only `"variant"` in uci format, a san-formatted variantformat 189 | is added by the dispatcher for client-display use. 190 | 191 | ## Board moves 192 | 193 | ### Move 194 | 195 | ```json 196 | { 197 | "cmd": "move", 198 | "uci": "move-in-uci-format (e.g. e2-e4, e8-g8, e7-e8Q, 0000)", 199 | "result": "empty, 1-0, 0-1, 1/2-1/2", 200 | "ponder": "t.b.d", 201 | "score": "optional (engine move) centi-pawn score or #2 mate announcement", 202 | "depth": "optional (engine move) search depth (half moves)", 203 | "seldepth": "optional (engine move) selective search depth (half moves)", 204 | "nps": "optional (engine move) nodes per second", 205 | "tbhits": "optional (engine move) table-base hits", 206 | "actor": "name-of-agent-sending-this" 207 | } 208 | ``` 209 | 210 | ### Take back move 211 | 212 | ```json 213 | { 214 | "cmd": "move_back", 215 | "actor": "name-of-agent-sending-this" 216 | } 217 | ``` 218 | 219 | ### Move forward 220 | 221 | ```json 222 | { 223 | "cmd": "move_forward", 224 | "actor": "name-of-agent-sending-this" 225 | } 226 | ``` 227 | 228 | ### Move to start of game 229 | 230 | ```json 231 | { 232 | "cmd": "move_start", 233 | "actor": "name-of-agent-sending-this" 234 | } 235 | ``` 236 | 237 | ### Move to end of game 238 | 239 | ```json 240 | { 241 | "cmd": "move_end", 242 | "actor": "name-of-agent-sending-this" 243 | } 244 | ``` 245 | 246 | ## Configuration messages 247 | 248 | ### update agent state 249 | 250 | ```json 251 | { 252 | "cmd": "agent_state", 253 | "state": "idle or busy or offline or online", 254 | "message": "optional message", 255 | "name": "Descriptive name", 256 | "version": "Version information", 257 | "authors": "authors in case of engine", 258 | "class": "agent class, e.g. engine, board", 259 | "actor": "name-of-agent-sending-this" 260 | } 261 | ``` 262 | 263 | ### Text encoding 264 | 265 | ```json 266 | { 267 | "cmd": "text_encoding", 268 | "unicode": true, 269 | "actor": "name-of-agent-sending-this" 270 | } 271 | ``` 272 | 273 | ### Chose depth of preview FEN 274 | 275 | ```json 276 | { 277 | "cmd": "preview_fen_depth", 278 | "depth": "number-of-half-moves-for-preview-position", 279 | "actor": "name-of-agent-sending-this" 280 | } 281 | ``` 282 | 283 | ### Get engine list 284 | 285 | Request list of engines 286 | 287 | ```json 288 | { 289 | "cmd": "get_engine_list", 290 | "actor": "name-of-agent-sending-this" 291 | } 292 | ``` 293 | 294 | ### Engine list 295 | 296 | List of all engines currently known, reply to `get_engine_list`. 297 | 298 | ```json 299 | { 300 | "cmd": "engine_list", 301 | "actor": "name-of-agent-sending-this", 302 | "engines": { 303 | "name-of-engine-1" : { 304 | "name": "name-of-engine", 305 | "active": true, 306 | "options": { 307 | "Threads": 1, 308 | "MultiPV": 4, 309 | "SyzygyPath": "path-to-syzygy-endgame-database", 310 | "Ponder": false, 311 | "UCI_Elo": 1800, 312 | "Hash": 16 313 | } 314 | }, 315 | ``` 316 | 317 | ... 318 | 319 | ```json 320 | "name-of-engine-n": { 321 | "name": "name-of-engine-n", 322 | "active": true, 323 | "options": { 324 | "Threads": 1, 325 | "MultiPV": 4, 326 | "SyzygyPath": "path-to-syzygy-endgame-database", 327 | "Ponder": false, 328 | "UCI_Elo": 1800, 329 | "Hash": 16 330 | } 331 | } 332 | } 333 | } 334 | ``` 335 | 336 | ### Select players 337 | 338 | ```json 339 | { 340 | "cmd": "select_players", 341 | "white": "human or name of uci engine", 342 | "black": "human or name of uci engine", 343 | 344 | "actor": "name-of-agent-sending-this" 345 | } 346 | ``` 347 | 348 | ## Hardware board specific messages 349 | 350 | ### Hardware board orientation 351 | 352 | ```json 353 | { 354 | "cmd": "turn_hardware_board", 355 | "actor": "name-of-agent-sending-this" 356 | } 357 | ``` 358 | 359 | ### Hardware board led mode 360 | 361 | ```json 362 | { 363 | "cmd": "led_info", 364 | "plies": "number of plies to visualise with board leds (max 4)", 365 | "actor": "name-of-agent-sending-this" 366 | } 367 | ``` 368 | 369 | ### Fetch hardware board position 370 | 371 | ```json 372 | { 373 | "cmd": "position_fetch", 374 | "from": "name-of-[hardware-]board-agent from which position should be fetched, e.g. 'ChessLinkAgent'", 375 | "actor": "name-of-agent-sending-this" 376 | } 377 | ``` 378 | 379 | ### Raw board position 380 | 381 | ```json 382 | { 383 | "cmd": "raw_board_position", 384 | "fen": "unchecked-postion-on-hardware-board-for-debugging", 385 | "actor": "name-of-agent-sending-this" 386 | } 387 | ``` 388 | -------------------------------------------------------------------------------- /mchess/chess_link_agent.py: -------------------------------------------------------------------------------- 1 | ''' Agent for Millennium chess board Chess Genius Exclusive ''' 2 | import logging 3 | import time 4 | import copy 5 | 6 | import chess 7 | import chess_link as cl 8 | 9 | 10 | class ChessLinkAgent: 11 | ''' Hardware board agent implementation ''' 12 | 13 | def __init__(self, appque, prefs, timeout=30): 14 | self.name = 'ChessLinkAgent' 15 | self.appque = appque 16 | self.prefs = prefs 17 | self.ply_vis_delay = prefs['ply_vis_delay'] 18 | self.log = logging.getLogger(self.name) 19 | self.cl_brd = cl.ChessLink(appque, self.name) 20 | self.init_position = False 21 | self.max_plies = prefs['max_plies_board'] 22 | 23 | if self.cl_brd.connected is True: 24 | self.cl_brd.get_version() 25 | self.cl_brd.set_debounce(4) 26 | self.cl_brd.get_scan_time_ms() 27 | self.cl_brd.set_scan_time_ms(100.0) 28 | self.cl_brd.get_scan_time_ms() 29 | self.cl_brd.get_position() 30 | else: 31 | self.log.warning("Connection to ChessLink failed.") 32 | return 33 | 34 | self.log.debug("waiting for board position") 35 | start = time.time() 36 | warned = False 37 | while time.time() - start < timeout and self.init_position is False: 38 | if self.cl_brd.error_condition is True: 39 | self.log.info("ChessLink board not available.") 40 | return 41 | if time.time() - start > 2 and warned is False: 42 | warned = True 43 | self.log.info( 44 | f"Searching for ChessLink board (max {timeout} secs)...") 45 | self.init_position = self.cl_brd.position_initialized() 46 | time.sleep(0.1) 47 | 48 | if self.init_position is True: 49 | self.log.debug("board position received, init ok.") 50 | else: 51 | self.log.error( 52 | f"no board position received within timeout {timeout}") 53 | 54 | def quit(self): 55 | self.cl_brd.quit() 56 | 57 | def agent_ready(self): 58 | return self.init_position 59 | 60 | def get_fen(self): 61 | return self.cl_brd.position_to_fen(self.cl_brd.position) 62 | 63 | def variant_to_positions(self, _board, moves, plies): 64 | board = copy.deepcopy(_board) 65 | pos = [] 66 | mvs = len(moves) 67 | if mvs > plies: 68 | mvs = plies 69 | 70 | try: 71 | pos.append(self.cl_brd.fen_to_position(board.fen())) 72 | for i in range(mvs): 73 | board.push(chess.Move.from_uci(moves[i])) 74 | pos.append(self.cl_brd.fen_to_position(board.fen())) 75 | for i in range(mvs): 76 | board.pop() 77 | except Exception as e: 78 | self.log.warning(f"Data corruption in variant_to_positions: {e}") 79 | return None 80 | return pos 81 | 82 | def color(self, col): 83 | if col == chess.WHITE: 84 | col = self.cl_brd.WHITE 85 | else: 86 | col = self.cl_brd.BLACK 87 | return col 88 | 89 | def visualize_variant(self, board, moves, plies=1, freq=-1): 90 | if freq == -1: 91 | freq = self.ply_vis_delay 92 | if plies > 4: 93 | plies = 4 94 | pos = self.variant_to_positions(board, moves, plies) 95 | if pos is not None: 96 | self.cl_brd.show_deltas(pos, freq) 97 | 98 | def display_info(self, _board, info): 99 | board = copy.deepcopy(_board) 100 | # if info['actor'] == self.prefs['computer_player_name']: 101 | if 'multipv_index' in info: 102 | if info['multipv_index'] == 1: # Main variant only 103 | if 'variant' in info: 104 | self.visualize_variant( 105 | board, info['variant'], plies=self.max_plies) 106 | else: 107 | self.log.error('Unexpected info-format') 108 | 109 | def set_valid_moves(self, board, val): 110 | if board.turn == chess.WHITE: 111 | col = self.cl_brd.WHITE 112 | else: 113 | col = self.cl_brd.BLACK 114 | self.cl_brd.move_from(board.fen(), val, col) 115 | -------------------------------------------------------------------------------- /mchess/chess_link_bluepy.py: -------------------------------------------------------------------------------- 1 | """ 2 | ChessLink transport implementation for Bluetooth LE connections using `bluepy`. 3 | """ 4 | import logging 5 | import threading 6 | import queue 7 | import time 8 | import os 9 | 10 | import chess_link_protocol as clp 11 | try: 12 | import bluepy 13 | from bluepy.btle import Scanner, DefaultDelegate, Peripheral 14 | bluepy_ble_support = True 15 | except ImportError: 16 | bluepy_ble_support = False 17 | 18 | 19 | class Transport(): 20 | """ 21 | ChessLink transport implementation for Bluetooth LE connections using `bluepy`. 22 | 23 | This class does automatic hardware detection of any ChessLink board using bluetooth LE 24 | and supports Linux and Raspberry Pi. 25 | 26 | This transport uses an asynchronous background thread for hardware communcation. 27 | All replies are written to the python queue `que` given during initialization. 28 | 29 | For the details of the Chess Link protocol, please refer to: 30 | `magic-link.md `_. 31 | """ 32 | 33 | def __init__(self, que, protocol_dbg=False): 34 | """ 35 | Initialize with python queue for event handling. 36 | Events are strings conforming to the ChessLink protocol as documented in 37 | `magic-link.md `_. 38 | 39 | :param que: Python queue that will eceive events from chess board. 40 | :param protocol_dbg: True: byte-level ChessLink protocol debug messages 41 | """ 42 | if bluepy_ble_support is False: 43 | self.init = False 44 | return 45 | self.wrque = queue.Queue() 46 | self.log = logging.getLogger("ChessLinkBluePy") 47 | self.que = que # asyncio.Queue() 48 | self.init = True 49 | self.log.debug("bluepy_ble init ok") 50 | self.protocol_debug = protocol_dbg 51 | self.scan_timeout = 10 52 | self.worker_thread_active = False 53 | self.worker_threader = None 54 | self.conn_state = None 55 | 56 | self.bp_path = os.path.dirname(os.path.abspath(bluepy.__file__)) 57 | self.bp_helper = os.path.join(self.bp_path, 'bluepy-helper') 58 | if not os.path.exists(self.bp_helper): 59 | self.log.warning(f'Unexpected: {self.bp_helper} does not exists!') 60 | self.fix_cmd = "sudo setcap 'cap_net_raw,cap_net_admin+eip' " + self.bp_helper 61 | 62 | def quit(self): 63 | """ 64 | Initiate worker-thread stop 65 | """ 66 | self.worker_thread_active = False 67 | 68 | def search_board(self, iface=0): 69 | """ 70 | Search for ChessLink connections using Bluetooth LE. 71 | 72 | :param iface: interface number of bluetooth adapter, default 1. 73 | :returns: Bluetooth address of ChessLink board, or None on failure. 74 | """ 75 | self.log.debug("bluepy_ble: searching for boards") 76 | 77 | class ScanDelegate(DefaultDelegate): 78 | ''' scanner class ''' 79 | 80 | def __init__(self, log): 81 | self.log = log 82 | DefaultDelegate.__init__(self) 83 | 84 | def handleDiscovery(self, scanEntry, isNewDev, isNewData): 85 | if isNewDev: 86 | self.log.debug( 87 | "Discovered device {}".format(scanEntry.addr)) 88 | elif isNewData: 89 | self.log.debug( 90 | "Received new data from {}".format(scanEntry.addr)) 91 | 92 | scanner = Scanner(iface=iface).withDelegate(ScanDelegate(self.log)) 93 | 94 | try: 95 | devices = scanner.scan(self.scan_timeout) 96 | except Exception as e: 97 | self.log.error(f"BLE scanning failed. {e}") 98 | self.log.error(f"excecute: {self.fix_cmd}") 99 | self.log.error("or (if that fails) start ONCE with: `sudo python mchess.py`" 100 | "(fix ownership of chess_link_config.json afterwards)") 101 | return None 102 | 103 | devs = sorted(devices, key=lambda x: x.rssi, reverse=True) 104 | for b in devs: 105 | self.log.debug(f'sorted by rssi {b.addr} {b.rssi}') 106 | 107 | for bledev in devs: 108 | self.log.debug( 109 | f"Device {bledev.addr} ({bledev.addrType}), RSSI={bledev.rssi} dB") 110 | for (adtype, desc, value) in bledev.getScanData(): 111 | self.log.debug(f" {desc} ({adtype}) = {value}") 112 | if desc == "Complete Local Name": 113 | if "MILLENNIUM CHESS" in value: 114 | self.log.info("Autodetected Millennium Chess Link board at " 115 | f"Bluetooth LE address: {bledev.addr}, " 116 | f"signal strength (rssi): {bledev.rssi}") 117 | return bledev.addr 118 | return None 119 | 120 | def test_board(self, address): 121 | """ 122 | Test dummy. 123 | 124 | :returns: Version string "1.0" always. 125 | """ 126 | self.log.debug(f"test_board address {address} not implemented.") 127 | # self.open_mt(address) 128 | return "1.0" 129 | 130 | def open_mt(self, address): 131 | """ 132 | Open a bluetooth LE connection to ChessLink board. 133 | 134 | :param address: bluetooth address 135 | :returns: True on success. 136 | """ 137 | self.log.debug('Starting worker-thread for bluepy ble') 138 | self.worker_thread_active = True 139 | self.worker_threader = threading.Thread( 140 | target=self.worker_thread, args=(self.log, address, self.wrque, self.que)) 141 | self.worker_threader.setDaemon(True) 142 | self.worker_threader.start() 143 | timer = time.time() 144 | self.conn_state = None 145 | while self.conn_state is None and time.time() - timer < 5.0: 146 | time.sleep(0.1) 147 | if self.conn_state is None: 148 | return False 149 | return self.conn_state 150 | 151 | def write_mt(self, msg): 152 | """ 153 | Encode and asynchronously write a message to ChessLink. 154 | 155 | :param msg: Message string. Parity will be added, and block CRC appended. 156 | """ 157 | if self.protocol_debug is True: 158 | self.log.debug(f'write-que-entry {msg}') 159 | self.wrque.put(msg) 160 | 161 | def get_name(self): 162 | """ 163 | Get name of this transport. 164 | 165 | :returns: 'chess_link_bluepy' 166 | """ 167 | return "chess_link_bluepy" 168 | 169 | def is_init(self): 170 | """ 171 | Check, if hardware connection is up. 172 | 173 | :returns: True on success. 174 | """ 175 | return self.init 176 | 177 | def agent_state(self, que, state, msg): 178 | que.put('agent-state: ' + state + ' ' + msg) 179 | 180 | def mil_open(self, address, mil, que, log): 181 | 182 | class PeriDelegate(DefaultDelegate): 183 | ''' peripheral delegate class ''' 184 | 185 | def __init__(self, log, que): 186 | self.log = log 187 | self.que = que 188 | self.log.debug("Init delegate for peri") 189 | self.chunks = "" 190 | DefaultDelegate.__init__(self) 191 | 192 | def handleNotification(self, cHandle, data): 193 | self.log.debug( 194 | "BLE: Handle: {}, data: {}".format(cHandle, data)) 195 | rcv = "" 196 | for b in data: 197 | rcv += chr(b & 127) 198 | self.log.debug('BLE received [{}]'.format(rcv)) 199 | self.chunks += rcv 200 | if self.chunks[0] not in clp.protocol_replies: 201 | self.log.warning( 202 | "Illegal reply start '{}' received, discarding".format(self.chunks[0])) 203 | while len(self.chunks) > 0 and self.chunks[0] not in clp.protocol_replies: 204 | self.chunks = self.chunks[1:] 205 | if len(self.chunks) > 0: 206 | mlen = clp.protocol_replies[self.chunks[0]] 207 | if len(self.chunks) >= mlen: 208 | valmsg = self.chunks[:mlen] 209 | self.log.debug( 210 | 'bluepy_ble received complete msg: {}'.format(valmsg)) 211 | if clp.check_block_crc(valmsg): 212 | que.put(valmsg) 213 | self.chunks = self.chunks[mlen:] 214 | 215 | rx = None 216 | tx = None 217 | log.debug('Peripheral generated {}'.format(address)) 218 | try: 219 | services = mil.getServices() 220 | except Exception as e: 221 | emsg = 'Failed to enumerate services for {}, {}'.format(address, e) 222 | log.error(emsg) 223 | self.agent_state(que, 'offline', emsg) 224 | return None, None 225 | # time.sleep(0.1) 226 | log.debug("services: {}".format(len(services))) 227 | for ser in services: 228 | log.debug('Service: {}'.format(ser)) 229 | chrs = ser.getCharacteristics() 230 | for chri in chrs: 231 | if chri.uuid == "49535343-1e4d-4bd9-ba61-23c647249616": # TX char, rx for us 232 | rx = chri 233 | rxh = chri.getHandle() 234 | # Enable notification magic: 235 | log.debug('Enabling notifications') 236 | mil.writeCharacteristic( 237 | rxh + 1, (1).to_bytes(2, byteorder='little')) 238 | if chri.uuid == "49535343-8841-43f4-a8d4-ecbe34729bb3": # RX char, tx for us 239 | tx = chri 240 | # txh = chri.getHandle() 241 | if chri.supportsRead(): 242 | log.debug(f" {chri} UUID={chri.uuid} {chri.propertiesToString()} -> " 243 | "{chri.read()}") 244 | else: 245 | log.debug( 246 | f" {chri} UUID={chri.uuid}{chri.propertiesToString()}") 247 | 248 | try: 249 | log.debug('Installing peripheral delegate') 250 | delegate = PeriDelegate(log, que) 251 | mil.withDelegate(delegate) 252 | except Exception as e: 253 | emsg = 'Bluetooth LE: Failed to install peripheral delegate! {}'.format( 254 | e) 255 | log.error(emsg) 256 | self.agent_state(que, 'offline', emsg) 257 | return None, None 258 | self.agent_state(que, 'online', 'Connected to ChessLink board via BLE') 259 | return (rx, tx) 260 | 261 | def worker_thread(self, log, address, wrque, que): 262 | """ 263 | Background thread that handles bluetooth sending and forwards data received via 264 | bluetooth to the queue `que`. 265 | """ 266 | mil = None 267 | message_delta_time = 0.1 # least 0.1 sec between outgoing btle messages 268 | 269 | rx = None 270 | tx = None 271 | log.debug("bluepy_ble open_mt {}".format(address)) 272 | # time.sleep(0.1) 273 | try: 274 | log.debug("per1") 275 | mil = Peripheral(address) 276 | log.debug("per2") 277 | except Exception as e: 278 | log.debug("per3") 279 | emsg = 'Failed to create BLE peripheral at {}, {}'.format( 280 | address, e) 281 | log.error(emsg) 282 | self.agent_state(que, 'offline', '{}'.format(e)) 283 | self.conn_state = False 284 | return 285 | 286 | rx, tx = self.mil_open(address, mil, que, log) 287 | 288 | time_last_out = time.time() + 0.2 289 | 290 | if rx is None or tx is None: 291 | bt_error = True 292 | self.conn_state = False 293 | else: 294 | bt_error = False 295 | self.conn_state = True 296 | while self.worker_thread_active is True: 297 | rep_err = False 298 | while bt_error is True: 299 | time.sleep(1) 300 | bt_error = False 301 | self.init = False 302 | try: 303 | mil.connect(address) 304 | except Exception as e: 305 | if rep_err is False: 306 | self.log.warning( 307 | f"Reconnect failed: {e} [Local bluetooth problem?]") 308 | rep_err = True 309 | bt_error = True 310 | if bt_error is False: 311 | self.log.info(f"Bluetooth reconnected to {address}") 312 | rx, tx = self.mil_open(address, mil, que, log) 313 | time_last_out = time.time() + 0.2 314 | self.init = True 315 | 316 | if wrque.empty() is False and time.time() - time_last_out > message_delta_time: 317 | msg = wrque.get() 318 | gpar = 0 319 | for b in msg: 320 | gpar = gpar ^ ord(b) 321 | msg = msg + clp.hex2(gpar) 322 | if self.protocol_debug is True: 323 | log.debug("blue_ble write: <{}>".format(msg)) 324 | bts = "" 325 | for c in msg: 326 | bo = chr(clp.add_odd_par(c)) 327 | bts += bo 328 | btsx = bts.encode('latin1') 329 | if self.protocol_debug is True: 330 | log.debug("Sending: <{}>".format(btsx)) 331 | try: 332 | tx.write(btsx, withResponse=True) 333 | time_last_out = time.time() 334 | except Exception as e: 335 | log.error(f"bluepy_ble: failed to write {msg}: {e}") 336 | bt_error = True 337 | self.agent_state( 338 | que, 'offline', f'Connected to Bluetooth peripheral lost: {e}') 339 | wrque.task_done() 340 | 341 | try: 342 | rx.read() 343 | mil.waitForNotifications(0.05) 344 | # time.sleep(0.1) 345 | except Exception as e: 346 | self.log.warning(f"Bluetooth error {e}") 347 | bt_error = True 348 | self.agent_state( 349 | que, 'offline', f'Connected to Bluetooth peripheral lost: {e}') 350 | continue 351 | 352 | log.debug('wt-end') 353 | -------------------------------------------------------------------------------- /mchess/chess_link_protocol.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Helper functions for the Chess Link protocol for character-based odd-parity and 4 | message-block-parity. 5 | 6 | The chess link protocol sends ASCII messages. Each ASCII character gets an additional 7 | odd-parity-bit. Each block of ASCII+odd-parity bytes gets an additional block parity. 8 | 9 | Details of the Chess Link protocol are documented in 10 | `magic-board.md `_. 11 | """ 12 | 13 | import logging 14 | 15 | protocol_replies = {'v': 7, 's': 67, 'l': 3, 'x': 3, 'w': 7, 'r': 7} 16 | 17 | 18 | def add_odd_par(b): 19 | """ 20 | The chess link protocol is 7-Bit ASCII. This adds an odd-parity-bit to an ASCII char 21 | 22 | :param b: an ASCII character (0..127) 23 | :returns: a byte (0..255) with odd parity in most significant bit. 24 | """ 25 | byte = ord(b) & 127 26 | par = 1 27 | for _ in range(7): 28 | bit = byte & 1 29 | byte = byte >> 1 30 | par = par ^ bit 31 | if par == 1: 32 | byte = ord(b) | 128 33 | else: 34 | byte = ord(b) & 127 35 | return byte 36 | 37 | 38 | def hexd(digit): 39 | """ 40 | Returns a hex digit '0'..'F' for an integer 0..15 41 | 42 | :param digit: integer 0..15 43 | :returns: an ASCII hex character '0'..'F' 44 | """ 45 | if digit < 10: 46 | return chr(ord('0') + digit) 47 | else: 48 | return chr(ord('A') - 10 + digit) 49 | 50 | 51 | def hex2(num): 52 | """ 53 | Convert integer to 2-digit hex string. Most numeric parameters and the block CRC 54 | are encoded as such 2-digit hex-string. 55 | 56 | :param num: uint_8 integer 0..255 57 | :returns: Returns a 2-digit hex code '00'..'FF' 58 | """ 59 | d1 = num // 16 60 | d2 = num % 16 61 | s = hexd(d1) + hexd(d2) 62 | return s 63 | 64 | 65 | def check_block_crc(msg): 66 | """ 67 | Chess link messages consist of 7-bit-ASCII characters with odd parity. At the end of each 68 | message, an additional block-parity is added. Valid chess link messages must have correct odd 69 | parity for each character and a valid block parity at the end. 70 | 71 | :param msg: a byte array with the message. 72 | :returns: True, if the last two bytes of msg contain a correct CRC, False otherwise. 73 | """ 74 | if len(msg) > 2: 75 | gpar = 0 76 | for b in msg[:-2]: 77 | gpar = gpar ^ ord(b) 78 | if msg[-2] + msg[-1] != hex2(gpar): 79 | logging.warning(f"CRC error rep={msg} CRCs: {ord(msg[-2])}!={hex2(gpar)}") 80 | return False 81 | else: 82 | return True 83 | else: 84 | logging.warning(f"Message {msg} too short for CRC check") 85 | return False 86 | 87 | 88 | def add_block_crc(msg): 89 | """Add block parity at the end of the message 90 | 91 | :param msg: a message byte array (each byte must have already been encoded with odd parity). 92 | This function adds two bytes of block CRC at the end of the message. 93 | :param msg: byte array with a message (incl. odd-parity bits set already) 94 | :return: two byte longer message that includes 2 CRC bytes. 95 | """ 96 | gpar = 0 97 | for b in msg: 98 | gpar = gpar ^ ord(b) 99 | msg = msg + hex2(gpar) 100 | return msg 101 | -------------------------------------------------------------------------------- /mchess/chess_link_pyblue.py: -------------------------------------------------------------------------------- 1 | ''' implementation framework for additional bluetooth module, not functional ''' 2 | import logging 3 | 4 | # import chess_link_protocol as clp 5 | 6 | # TODO: expand empty framework with actual functionality! 7 | 8 | 9 | class Transport(): 10 | ''' non-functional frame ''' 11 | def __init__(self, que): 12 | self.log = logging.getLogger("ChessLinkPyBlue") 13 | self.que = que # asyncio.Queue() 14 | self.init = True 15 | self.is_open = False 16 | self.log.debug("init ok") 17 | 18 | def search_board(self): 19 | self.log.debug("searching for boards") 20 | 21 | return None 22 | 23 | def test_board(self, address): 24 | return None 25 | 26 | def open_mt(self, address): 27 | self.log.debug(f"open_mt {address}") 28 | return False 29 | 30 | def write_mt(self, msg): 31 | return False 32 | 33 | def get_name(self): 34 | return "chess_link_pyblue" 35 | 36 | def is_init(self): 37 | return self.init 38 | -------------------------------------------------------------------------------- /mchess/chess_link_usb.py: -------------------------------------------------------------------------------- 1 | """ 2 | ChessLink transport implementation for USB connections. 3 | """ 4 | import logging 5 | import threading 6 | import time 7 | 8 | import chess_link_protocol as clp 9 | 10 | try: 11 | import serial 12 | import serial.tools.list_ports 13 | usb_support = True 14 | except ImportError: 15 | usb_support = False 16 | 17 | 18 | class Transport(): 19 | """ 20 | ChessLink transport implementation for USB connections. 21 | 22 | This class does automatic hardware detection of any ChessLink board connected 23 | via USB and support Linux, macOS and Windows. 24 | 25 | This transport uses an asynchronous background thread for hardware communcation. 26 | All replies are written to the python queue `que` given during initialization. 27 | """ 28 | 29 | def __init__(self, que, protocol_dbg=False): 30 | """ 31 | Initialize with python queue for event handling. 32 | Events are strings conforming to the ChessLink protocol as documented in 33 | `magic-link.md `_. 34 | 35 | :param que: Python queue that will eceive events from chess board. 36 | :param protocol_dbg: True: byte-level ChessLink protocol debug messages 37 | """ 38 | self.log = logging.getLogger("ChessLinkUSB") 39 | if usb_support is False: 40 | self.log.error( 41 | 'Cannot communicate: PySerial module not installed.') 42 | self.init = False 43 | return 44 | self.que = que # asyncio.Queue() 45 | self.init = True 46 | self.log.debug("USB init ok") 47 | self.protocol_debug = protocol_dbg 48 | self.last_agent_state = None 49 | self.error_state = False 50 | self.thread_active = False 51 | self.event_thread = None 52 | self.usb_dev = None 53 | self.uport = None 54 | 55 | def quit(self): 56 | """ 57 | Initiate worker-thread stop 58 | """ 59 | self.thread_active = False 60 | 61 | def search_board(self, iface=None): 62 | """ 63 | Search for ChessLink connections on all USB ports. 64 | 65 | :param iface: not used for USB. 66 | :returns: Name of the port with a ChessLink board, None on failure. 67 | """ 68 | self.log.info("Searching for ChessLink boards...") 69 | self.log.info('Note: search can be disabled in < chess_link_config.json >' 70 | ' by setting {"autodetect": false}') 71 | port = None 72 | ports = self.usb_port_search() 73 | if len(ports) > 0: 74 | if len(ports) > 1: 75 | self.log.warning(f"Found {len(ports)} Millennium boards, using first found.") 76 | port = ports[0] 77 | self.log.info(f"Autodetected Millennium board at USB port: {port}") 78 | return port 79 | 80 | def test_board(self, port): 81 | """ 82 | Test an usb port for correct answer on get version command. 83 | 84 | :returns: Version string on ok, None on failure. 85 | """ 86 | self.log.debug(f"Testing port: {port}") 87 | try: 88 | self.usb_dev = serial.Serial(port, 38400, timeout=2) 89 | self.usb_dev.dtr = 0 90 | self.write_mt("V") 91 | version = self.usb_read_synchr(self.usb_dev, 'v', 7) 92 | if len(version) != 7: 93 | self.usb_dev.close() 94 | self.log.debug(f"Message length {len(version)} instead of 7") 95 | return None 96 | if version[0] != 'v': 97 | self.log.debug(f"Unexpected reply {version}") 98 | self.usb_dev.close() 99 | return None 100 | verstring = f'{version[1]+version[2]}.{version[3]+version[4]}' 101 | self.log.debug(f"Millennium {verstring} at {port}") 102 | self.usb_dev.close() 103 | return verstring 104 | except (OSError, serial.SerialException) as e: 105 | self.log.debug(f'Board detection on {port} resulted in error {e}') 106 | try: 107 | self.usb_dev.close() 108 | except Exception: 109 | pass 110 | return None 111 | 112 | def usb_port_check(self, port): 113 | """ 114 | Check usb port for valid ChessLink connection 115 | 116 | :returns: True on success, False on failure. 117 | """ 118 | self.log.debug(f"Testing port: {port}") 119 | try: 120 | s = serial.Serial(port, 38400) 121 | s.close() 122 | return True 123 | except (OSError, serial.SerialException) as e: 124 | self.log.debug(f"Can't open port {port}, {e}") 125 | return False 126 | 127 | def usb_port_search(self): 128 | """ 129 | Get a list of all usb ports that have a connected ChessLink board. 130 | 131 | :returns: array of usb port names with valid ChessLink boards, an empty array 132 | if none is found. 133 | """ 134 | ports = list([port.device for port in serial.tools.list_ports.comports(True)]) 135 | vports = [] 136 | for port in ports: 137 | if self.usb_port_check(port): 138 | version = self.test_board(port) 139 | if version is not None: 140 | self.log.debug(f"Found board at: {port}") 141 | vports.append(port) 142 | break # only one port necessary 143 | return vports 144 | 145 | def write_mt(self, msg): 146 | """ 147 | Encode and write a message to ChessLink. 148 | 149 | :param msg: Message string. Parity will be added, and block CRC appended. 150 | """ 151 | msg = clp.add_block_crc(msg) 152 | bts = [] 153 | for c in msg: 154 | bo = clp.add_odd_par(c) 155 | bts.append(bo) 156 | try: 157 | if self.protocol_debug is True: 158 | self.log.debug(f'Trying write <{bts}>') 159 | self.usb_dev.write(bts) 160 | self.usb_dev.flush() 161 | except Exception as e: 162 | self.log.error(f"Failed to write {msg}: {e}") 163 | self.error_state = True 164 | return False 165 | if self.protocol_debug is True: 166 | self.log.debug(f"Written '{msg}' as < {bts} > ok") 167 | return True 168 | 169 | def usb_read_synchr(self, usbdev, cmd, num): 170 | """ 171 | Synchronous reads for initial hardware detection. 172 | """ 173 | rep = [] 174 | start = False 175 | while start is False: 176 | try: 177 | b = chr(ord(usbdev.read()) & 127) 178 | except Exception as e: 179 | self.log.debug(f"USB read failed: {e}") 180 | return [] 181 | if b == cmd: 182 | rep.append(b) 183 | start = True 184 | for _ in range(num - 1): 185 | try: 186 | b = chr(ord(usbdev.read()) & 127) 187 | rep.append(b) 188 | except (Exception) as e: 189 | self.log.error(f"Read error {e}") 190 | break 191 | if clp.check_block_crc(rep) is False: 192 | return [] 193 | return rep 194 | 195 | def agent_state(self, que, state, msg): 196 | if state != self.last_agent_state: 197 | self.last_agent_state = state 198 | que.put('agent-state: ' + state + ' ' + msg) 199 | 200 | def open_mt(self, port): 201 | """ 202 | Open an usb port to a connected ChessLink board. 203 | 204 | :returns: True on success. 205 | """ 206 | self.uport = port 207 | try: 208 | self.usb_dev = serial.Serial(port, 38400, timeout=0.1) 209 | self.usb_dev.dtr = 0 210 | except Exception as e: 211 | emsg = f'USB cannot open port {port}, {e}' 212 | self.log.error(emsg) 213 | self.agent_state(self.que, 'offline', emsg) 214 | return False 215 | self.log.debug(f'USB port {port} open') 216 | self.thread_active = True 217 | self.event_thread = threading.Thread( 218 | target=self.event_worker_thread, args=(self.que,)) 219 | self.event_thread.setDaemon(True) 220 | self.event_thread.start() 221 | return True 222 | 223 | def event_worker_thread(self, que): 224 | """ 225 | Background thread that sends data received via usb to the queue `que`. 226 | """ 227 | self.log.debug('USB worker thread started.') 228 | cmd_started = False 229 | cmd_size = 0 230 | cmd = "" 231 | self.agent_state(self.que, 'online', f'Connected to {self.uport}') 232 | self.error_state = False 233 | posted = False 234 | while self.thread_active: 235 | while self.error_state is True: 236 | time.sleep(1.0) 237 | try: 238 | self.usb_dev.close() 239 | except Exception as e: 240 | self.log.debug(f'Failed to close usb: {e}') 241 | try: 242 | self.usb_dev = serial.Serial( 243 | self.uport, 38400, timeout=0.1) 244 | self.usb_dev.dtr = 0 245 | self.agent_state(self.que, 'online', f'Reconnected to {self.uport}') 246 | self.error_state = False 247 | posted = False 248 | break 249 | except Exception as e: 250 | if posted is False: 251 | emsg = f"Failed to reconnected to {self.uport}, {e}" 252 | self.log.warning(emsg) 253 | self.agent_state(self.que, 'offline', emsg) 254 | posted = True 255 | 256 | b = "" 257 | try: 258 | if cmd_started is False: 259 | self.usb_dev.timeout = None 260 | else: 261 | self.usb_dev.timeout = 0.2 262 | by = self.usb_dev.read() 263 | if len(by) > 0: 264 | b = chr(ord(by) & 127) 265 | else: 266 | continue 267 | except Exception as e: 268 | if len(cmd) > 0: 269 | self.log.debug(f"USB command '{cmd[0]}' interrupted: {e}") 270 | time.sleep(0.1) 271 | cmd_started = False 272 | cmd_size = 0 273 | cmd = "" 274 | self.error_state = True 275 | continue 276 | if len(b) > 0: 277 | if cmd_started is False: 278 | if b in clp.protocol_replies: 279 | cmd_started = True 280 | cmd_size = clp.protocol_replies[b] 281 | cmd = b 282 | cmd_size -= 1 283 | else: 284 | cmd += b 285 | cmd_size -= 1 286 | if cmd_size == 0: 287 | cmd_started = False 288 | cmd_size = 0 289 | if self.protocol_debug is True: 290 | self.log.debug(f"USB received cmd: {cmd}") 291 | if clp.check_block_crc(cmd): 292 | que.put(cmd) 293 | cmd = "" 294 | 295 | def get_name(self): 296 | """ 297 | Get name of this transport. 298 | 299 | :returns: 'chess_link_usb' 300 | """ 301 | return "chess_link_usb" 302 | 303 | def is_init(self): 304 | """ 305 | Check, if hardware connection is up. 306 | 307 | :returns: True on success. 308 | """ 309 | self.log.debug("Ask for init") 310 | return self.init 311 | -------------------------------------------------------------------------------- /mchess/engines/engine-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "engine-name", 3 | "path": "path-to-engine-executable", 4 | "active": true 5 | } -------------------------------------------------------------------------------- /mchess/magic-board.md: -------------------------------------------------------------------------------- 1 | # ChessLink: Magic Chessboard Communications Protocol 2 | 3 | Version: _Dave Woodfield 25/10/17_ 4 | 5 | Data transfer to/from the Magic Chessboard (MB) will be at 38400 baud, odd parity, 7 bit, 1 stop. 6 | All data is printable ASCII. All data strings are terminated in a block-parity check character. 7 | Commands will only be actioned and acknowledged by the MB if the format and X-Y parity check is correct. 8 | The odd X parity will be performed by the USB serial driver. The Y parity is generated by taking the binary XOR of all 9 | of the transmitted ASCII characters including the command character and all of the data characters upto the check-byte. 10 | This value is then transmitted as a further 2 hex characters. 11 | 12 | ## Command 'S' - Status 13 | 14 | This command retrieves the piece status of the complete board. 15 | 16 | ``` 17 | S Command character 18 | Block parity check byte - 2 hex digits 19 | 20 | Reply:- 21 | 22 | s Lower-case acknowledge character 23 | .......... 64 * Piece codes. Data order is A8...H8,A7...H7 etc. 24 | Block parity check byte - 2 hex digits 25 | ``` 26 | `` are K,Q,R,N,B,P for white pieces and k,q,r,n,b,p for black. An empty square returns `'.'` . 27 | 28 | ## Command 'L' - LED 29 | 30 | The LED command sets the flash pattern of all 81 LEDs on the board. 31 | 32 | ``` 33 | L Command character 34 | XX Slot time – 2 hex digits 35 | .......... 81 * LED codes, LED 1 to LED 81 36 | Block parity check byte - 2 hex digits 37 | 38 | Reply:- 39 | 40 | l Lower-case acknowledge character 41 | Block parity check byte - 2 hex digits 42 | ``` 43 | 44 | `` parameters consist of 2 upper-case hex characters representing an LED pattern byte. The LEDs follow the bit pattern from b7 to b0, changing every LED time slot, e.g. = C4 will give an ON, ON, OFF, OFF, OFF, ON, OFF, OFF flash pattern repeating every 8 time slots. The slot time is set in units of 4.096mS. 45 | 46 | LED 1 is in the A8 corner, LED 9 is in the A1 corner 47 | 48 | LED 73 is in the H8 corner, LED 81 is in the H1 corner 49 | 50 | ## Command 'X' – eXtinguish all LEDs 51 | 52 | ``` 53 | X Command character 54 | Block parity check byte - 2 hex digits 55 | 56 | Reply:- 57 | 58 | x Lower-case acknowledge character 59 | Block parity check byte - 2 hex digits 60 | ``` 61 | 62 | ## Command 'T' - reseT 63 | 64 | This command will cause a hardware reset of the MB. There will be a start-up delay of 3 seconds and the MB will then start scanning again. Note that status and LED arrays will be cleared. 65 | 66 | ``` 67 | T Command character 68 | Block parity check byte - 2 hex digits 69 | ``` 70 | 71 | There will be no reply as a hardware reset is performed immediately upon correct receipt of this command. 72 | 73 | ## Command 'V' - Version 74 | 75 | ``` 76 | V Command character 77 | Block parity check byte - 2 hex digits 78 | 79 | Reply:- 80 | v Lower-case acknowledge character 81 | XX Firmware version number high – 2 hex digits 82 | XX Firmware version number low – 2 hex digits 83 | Block parity check byte - 2 hex digits 84 | ``` 85 | 86 | ## Command 'W' – Write E2ROM 87 | 88 | This command is used to write the E2 memory in the MB where operating parameters are stored. All values are transferred as upper-case hex characters. E2 locations above 0x10 may be used for other non-volatile data if required by the host. 89 | 90 | ``` 91 | W Command character 92 | XX Address – 2 hex digits 93 | XX Data byte – 2 hex digits 94 | Block parity check byte - 2 hex digits 95 | 96 | Reply:- 97 | 98 | w Lower-case acknowledge character 99 | XX Address – 2 hex digits 100 | XX Data byte – 2 hex digits 101 | Block parity check byte - 2 hex digits 102 | ``` 103 | 104 | ## Command 'R' – Read E2ROM 105 | 106 | This command is used to read the E2 memory in the MB where operating parameters are stored. All values are transferred as upper-case hex characters. 107 | 108 | ``` 109 | R Command character 110 | XX Address – 2 hex digits 111 | Block parity check byte - 2 hex digits 112 | 113 | Reply:- 114 | 115 | r Lower-case acknowledge character 116 | XX Address – 2 hex digits 117 | XX Data byte – 2 hex digits 118 | Block parity check byte - 2 hex digits 119 | ``` 120 | 121 | ### E2ROM option bytes: 122 | 123 | #### Address 00 = Serial port setup 124 | 125 | The block parity should normally be enabled, but can be disabled to enable easier testing with a terminal emulator. 126 | 127 | ``` 128 | b0 = Block parity disable 129 | 0 Enabled (default) 130 | 1 Disabled 131 | 132 | b7-b1 Unused 133 | ``` 134 | 135 | #### Address 01 = Scan time 136 | This is the time in units of 2.048mS to do a complete scan of the board. It defaults to 20 giving a scan time of 40.96mS, or 24.4 scans per second. This is a safe value which will work reliably in all conditions with all playing pieces, however if all of the pieces in a set give their nominal or faster response this value can be reduced by trial and error to a theoretical minimum of 15, giving a scan rate in excess of 32 scans per second. 137 | 138 | The scan time can be increased if desired to reduce the number of status messages that may be sent as a piece is swept across the board, but this can increase the delay before a position change may be seen. For speed chess the scan time should be short to get an almost immediate response and any move debounce done in the host software. 139 | 140 | The minimum allowed value is 15, anything less than this will set 20. The maximum value is 255 which will give 1.9 scans per second. 141 | 142 | #### Address 02 = Automatic reports 143 | 144 | If enabled automatic reports may by inserted between any command and it's acknowledgement. It is therefore important that all acknowledge messages have their type checked to match then up with the command that instigated them. The format is the same as a reply to the board status command. 145 | 146 | ``` 147 | b2-b0 = Automatic status reports 148 | 000 Send status on every scan (default) 149 | 001 Disabled. Use 'S' command 150 | 010 Send status with time set at address 03 151 | 011 Send status on any change 152 | 100 Send status on any change with 2 scan debounce 153 | 101 Send status on any change with 3 scan debounce 154 | 110 Send status on any change with 4 scan debounce 155 | 111 Send status on any change with 5 scan debounce 156 | 157 | b7-b3 Unused 158 | ``` 159 | 160 | #### Address 03 = Automatic status report time 161 | 162 | This is the time between automatic status reports if enabled, in units of 4.096mS. 163 | 164 | #### Address 04 = LED brightness 165 | 166 | ``` 167 | 0 = Dim, >14 = Full brightness 168 | ``` 169 | -------------------------------------------------------------------------------- /mchess/requirements.txt: -------------------------------------------------------------------------------- 1 | # https://docs.python.org/3/tutorial/venv.html 2 | 3 | pyserial 4 | chess==1.10.0 5 | pillow 6 | aiohttp 7 | # bluepy 8 | -------------------------------------------------------------------------------- /mchess/resources/pieces/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/b.png -------------------------------------------------------------------------------- /mchess/resources/pieces/bb60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/bb60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/bb64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/bb64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/bk60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/bk60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/bk64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/bk64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/bn60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/bn60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/bn64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/bn64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/bp60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/bp60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/bp64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/bp64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/bq60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/bq60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/bq64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/bq64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/br60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/br60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/br64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/br64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/license.md: -------------------------------------------------------------------------------- 1 | From: https://commons.wikimedia.org/wiki/Template:SVG_chess_pieces 2 | by [Cburnett](https://en.wikipedia.org/wiki/User:Cburnett) 3 | 4 | Licensed as: GFDL & BSD & GPL 5 | 6 | BSD: 7 | 8 | Copyright © The author 9 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 10 | 11 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | Neither the name of The author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /mchess/resources/pieces/wb60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wb60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wb64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wb64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wk60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wk60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wk64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wk64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wn60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wn60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wn64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wn64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wp60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wp60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wp64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wp64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wq60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wq60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wq64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wq64.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wr60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wr60.png -------------------------------------------------------------------------------- /mchess/resources/pieces/wr64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/resources/pieces/wr64.png -------------------------------------------------------------------------------- /mchess/terminal_agent.py: -------------------------------------------------------------------------------- 1 | ''' Simple terminal agent ''' 2 | import logging 3 | import time 4 | import sys 5 | import platform 6 | import threading 7 | import copy 8 | 9 | import chess 10 | 11 | 12 | class TerminalAgent: 13 | def __init__(self, appque, prefs): 14 | self.name = 'TerminalAgent' 15 | self.prefs = prefs 16 | self.log = logging.getLogger("TerminalAgent") 17 | self.appque = appque 18 | self.orientation = True 19 | self.active = False 20 | self.show_infos = True 21 | self.max_plies = 6 22 | if 'max_plies_terminal' in prefs: 23 | self.max_plies = prefs['max_plies_terminal'] 24 | if self.max_plies <= 0: 25 | self.show_infos = False 26 | self.display_cache = "" 27 | self.last_cursor_up = 0 28 | self.move_cache = "" 29 | self.info_cache = "" 30 | self.info_provider = {} 31 | self.max_mpv = 1 32 | 33 | self.kbd_moves = [] 34 | self.figrep = {"int": [1, 2, 3, 4, 5, 6, 0, -1, -2, -3, -4, -5, -6], 35 | "pythc": [(chess.PAWN, chess.WHITE), (chess.KNIGHT, chess.WHITE), (chess.BISHOP, chess.WHITE), 36 | (chess.ROOK, chess.WHITE), (chess.QUEEN, chess.WHITE), (chess.KING, chess.WHITE), 37 | (chess.PAWN, chess.BLACK), (chess.KNIGHT, chess.BLACK), (chess.BISHOP, chess.BLACK), 38 | (chess.ROOK, chess.BLACK), (chess.QUEEN, chess.BLACK), (chess.KING, chess.BLACK)], 39 | "unic": "♟♞♝♜♛♚ ♙♘♗♖♕♔", 40 | "ascii": "PNBRQK.pnbrqk"} 41 | self.chesssym = {"unic": ["-", "×", "†", "‡", "½"], 42 | "ascii": ["-", "x", "+", "#", "1/2"]} 43 | 44 | # this seems to set windows terminal to Unicode. There should be a better way. 45 | if platform.system().lower() == 'windows': 46 | from ctypes import windll, c_int, byref 47 | stdout_handle = windll.kernel32.GetStdHandle(c_int(-11)) 48 | mode = c_int(0) 49 | windll.kernel32.GetConsoleMode(c_int(stdout_handle), byref(mode)) 50 | mode = c_int(mode.value | 4) 51 | windll.kernel32.SetConsoleMode(c_int(stdout_handle), mode) 52 | 53 | self.keyboard_handler() 54 | 55 | def agent_ready(self): 56 | return self.active 57 | 58 | def quit(self): 59 | for _ in range(self.last_cursor_up): 60 | print() 61 | self.kdb_thread_active = False 62 | 63 | def position_to_text(self, brd): 64 | use_unicode_chess_figures = self.prefs['use_unicode_figures'] 65 | invert = self.prefs['invert_term_color'] 66 | board = copy.deepcopy(brd) 67 | tpos = [] 68 | tpos.append( 69 | " +------------------------+") 70 | for y in reversed(range(8)): 71 | ti = "{} |".format(y + 1) 72 | for x in range(8): 73 | f = board.piece_at(chess.square(x, y)) 74 | if (x + y) % 2 == 0 and use_unicode_chess_figures is True: 75 | invinv = invert 76 | else: 77 | invinv = not invert 78 | c = '?' 79 | # for i in range(len(self.figrep['int'])): 80 | if f is None: 81 | c = ' ' 82 | else: 83 | if use_unicode_chess_figures is True: 84 | c = f.unicode_symbol(invert_color=invinv) 85 | else: 86 | c = f.symbol() 87 | # if ((self.figrep['pythc'][i][1] == f.color) == inv) and self.figrep['pythc'][i][0] == f.piece_type: 88 | # if use_unicode_chess_figures is True: 89 | # c = self.figrep['unic'][i] 90 | # else: 91 | # c = self.figrep['ascii'][i] 92 | # break 93 | if (x + y) % 2 == 0: 94 | ti += "\033[7m {} \033[m".format(c) 95 | else: 96 | ti += " {} ".format(c) 97 | ti += "|" 98 | tpos.append(ti) 99 | tpos.append( 100 | " +------------------------+") 101 | tpos.append(" A B C D E F G H ") 102 | return tpos 103 | 104 | def moves_to_text(self, brd, score=None, lines=11): 105 | use_unicode_chess_figures = self.prefs['use_unicode_figures'] 106 | invert = self.prefs['invert_term_color'] 107 | board = copy.deepcopy(brd) 108 | ams = ["" for _ in range(11)] 109 | mc = len(board.move_stack) 110 | if board.turn == chess.BLACK: 111 | mmc = 2 * lines - 1 112 | else: 113 | mmc = 2 * lines 114 | if mc > mmc: 115 | mc = mmc 116 | move_store = [] 117 | 118 | amsi = lines - 1 119 | for i in range(mc): 120 | if amsi < 0: 121 | logging.error("bad amsi index! {}".format(amsi)) 122 | if board.is_checkmate() is True: 123 | if use_unicode_chess_figures is True: 124 | chk = self.chesssym['unic'][3] 125 | else: 126 | chk = self.chesssym['ascii'][3] 127 | elif board.is_check() is True: 128 | if use_unicode_chess_figures is True: 129 | chk = self.chesssym['unic'][2] 130 | else: 131 | chk = self.chesssym['ascii'][2] 132 | else: 133 | chk = "" 134 | l1 = len(board.piece_map()) 135 | mv = board.pop() 136 | if mv == chess.Move.null(): 137 | move = '{:10s}'.format('--') 138 | else: 139 | l2 = len(board.piece_map()) 140 | move_store.append(mv) 141 | if l1 != l2: # capture move, piece count changed :-/ 142 | if use_unicode_chess_figures is True: 143 | sep = self.chesssym['unic'][1] 144 | else: 145 | sep = self.chesssym['ascii'][1] 146 | else: 147 | if use_unicode_chess_figures is True: 148 | sep = self.chesssym['unic'][0] 149 | else: 150 | sep = self.chesssym['ascii'][0] 151 | if mv.promotion is not None: 152 | # TODO: cleanup fig-code generation 153 | if use_unicode_chess_figures is True: 154 | try: 155 | fig = board.piece_at(mv.from_square).unicode_symbol( 156 | invert_color=not invert) 157 | except Exception as e: 158 | self.log.error( 159 | "Move contains empty origin: {}".format(e)) 160 | fig = "?" 161 | else: 162 | try: 163 | fig = board.piece_at(mv.from_square).symbol() 164 | except Exception as e: 165 | self.log.error( 166 | "Move contains empty origin: {}".format(e)) 167 | fig = "?" 168 | if use_unicode_chess_figures is True: 169 | try: 170 | pro = chess.Piece(mv.promotion, board.piece_at( 171 | mv.from_square).color).unicode_symbol(invert_color=not invert) 172 | except Exception as e: 173 | self.log.error( 174 | "Move contains empty origin: {}".format(e)) 175 | pro = "?" 176 | else: 177 | try: 178 | # pro = mv.promotion.symbol() 179 | pro = chess.Piece(mv.promotion, board.piece_at( 180 | mv.from_square).color).symbol() 181 | except Exception as e: 182 | self.log.error( 183 | "Move contains empty origin: {}".format(e)) 184 | pro = "?" 185 | else: 186 | pro = "" 187 | if use_unicode_chess_figures is True: 188 | try: 189 | fig = board.piece_at(mv.from_square).unicode_symbol( 190 | invert_color=not invert) 191 | except Exception as e: 192 | self.log.error( 193 | "Move contains empty origin: {}".format(e)) 194 | fig = "?" 195 | else: 196 | try: 197 | fig = board.piece_at(mv.from_square).symbol() 198 | except Exception as e: 199 | self.log.error( 200 | "Move contains empty origin: {}".format(e)) 201 | fig = "?" 202 | move = '{:10s}'.format( 203 | fig + " " + chess.SQUARE_NAMES[mv.from_square] + sep + chess.SQUARE_NAMES[mv.to_square] + pro + chk) 204 | 205 | if amsi == lines - 1 and score is not None: 206 | move = '{} ({})'.format(move, score) 207 | score = '' 208 | 209 | ams[amsi] = move + ams[amsi] 210 | if board.turn == chess.WHITE: 211 | ams[amsi] = "{:3d}. ".format(board.fullmove_number) + ams[amsi] 212 | amsi = amsi - 1 213 | 214 | for i in reversed(range(len(move_store))): 215 | board.push(move_store[i]) 216 | 217 | return ams 218 | 219 | def cursor_up(self, n=1): 220 | # Windows: cursor up by n: ESC [ A 221 | # ANSI: cursor up by n: ESC [ A 222 | # ESC=\033, 27 223 | esc = chr(27) 224 | print("{}[{}A".format(esc, n), end="") 225 | 226 | def display_board(self, board, attribs): 227 | txa = self.position_to_text(board) 228 | 229 | ams = self.moves_to_text(board, lines=len(txa)) 230 | header = ' {:>10.10s} - {:10.10s}'.format( 231 | attribs['white_name'], attribs['black_name']) 232 | new_cache = header 233 | for i in range(len(txa)): 234 | col = ' ' 235 | if (board.turn == chess.WHITE) and (i == 8): 236 | col = '<-' 237 | if (board.turn == chess.BLACK) and (i == 1): 238 | col = '<-' 239 | new_cache += '{}{}{}'.format(txa[i], col, ams[i]) 240 | if new_cache == self.display_cache: 241 | self.log.debug("Unnecessary display_board call") 242 | return 243 | self.display_cache = new_cache 244 | print(header) 245 | for i in range(len(txa)): 246 | col = ' ' 247 | if (board.turn == chess.WHITE) and (i == 8): 248 | col = '<-' 249 | if (board.turn == chess.BLACK) and (i == 1): 250 | col = '<-' 251 | print('{}{}{}'.format(txa[i], col, ams[i])) 252 | 253 | def agent_states(self, msg): 254 | print('State of agent {} changed to {}, {}'.format( 255 | msg['actor'], msg['state'], msg['message'])) 256 | 257 | def display_move(self, move_msg): 258 | if 'score' in move_msg: 259 | new_move = '\nMove {} (ev: {}) by {}'.format( 260 | move_msg['uci'], move_msg['score'], move_msg['actor']) 261 | else: 262 | new_move = '\nMove {} by {}'.format( 263 | move_msg['uci'], move_msg['actor']) 264 | if 'ponder' in move_msg: 265 | new_move += '\nPonder: {}'.format(move_msg['ponder']) 266 | 267 | if 'result' in move_msg and move_msg['result'] != '': 268 | new_move += f" ({move_msg['result']})" 269 | 270 | if new_move != self.move_cache: 271 | for _ in range(self.last_cursor_up): 272 | print() 273 | self.last_cursor_up = 0 274 | self.move_cache = new_move 275 | print(new_move) 276 | print() 277 | else: 278 | self.log.debug( 279 | "Unnecessary repetion of move-print suppressed by cache") 280 | self.info_cache = "" 281 | self.info_provider = {} 282 | self.max_mpv = 1 283 | for ac in self.info_provider: 284 | self.info_provider[ac] = {} 285 | 286 | def display_info(self, board, info): 287 | if self.show_infos is False: 288 | return 289 | mpv_ind = info['multipv_index'] # index to multipv-line number 1.. 290 | if mpv_ind > self.max_mpv: 291 | self.max_mpv = mpv_ind 292 | 293 | header = '[' 294 | if 'actor' in info: 295 | header += info['actor'] + ' ' 296 | if 'nps' in info: 297 | header += 'Nps: {} '.format(info['nps']) 298 | if 'depth' in info: 299 | d = 'Depth: {}'.format(info['depth']) 300 | if 'seldepth' in info: 301 | d += '/{} '.format(info['seldepth']) 302 | header += d 303 | if 'appque' in info: 304 | header += 'AQue: {} '.format(info['appque']) 305 | if 'tbhits' in info: 306 | header += 'TB: {}] '.format(info['tbhits']) 307 | else: 308 | header += '] ' 309 | 310 | variant = '({}) '.format(mpv_ind) 311 | if 'score' in info: 312 | variant += '{} '.format(info['score']) 313 | if 'san_variant' in info: 314 | moves = info['san_variant'] 315 | mvs = len(moves) 316 | if mvs > self.max_plies: 317 | mvs = self.max_plies 318 | for i in range(mvs): 319 | if i > 0: 320 | variant += ' ' 321 | variant += f"{moves[i][1]} " 322 | 323 | if info['actor'] not in self.info_provider: 324 | self.info_provider[info['actor']] = {} 325 | self.info_provider[info['actor']]['header'] = header 326 | self.info_provider[info['actor']][mpv_ind] = variant 327 | 328 | cst = "" 329 | for ac in self.info_provider: 330 | for k in self.info_provider[ac]: 331 | cst += self.info_provider[ac][k] 332 | if cst != self.info_cache: 333 | self.info_cache = cst 334 | n = 0 335 | for ac in self.info_provider: 336 | if 'header' not in self.info_provider[ac]: 337 | continue 338 | print('{:80s}'.format(self.info_provider[ac]['header'][:80])) 339 | n += 1 340 | for i in range(1, self.max_mpv + 1): 341 | if i in self.info_provider[ac]: 342 | print('{:80s}'.format(self.info_provider[ac][i][:80])) 343 | n += 1 344 | self.cursor_up(n) 345 | self.last_cursor_up = n 346 | else: 347 | self.log.debug("Suppressed redundant display_info") 348 | 349 | def set_valid_moves(self, board, vals): 350 | self.kbd_moves = [] 351 | if vals is not None: 352 | for v in vals: 353 | self.kbd_moves.append(vals[v]) 354 | 355 | def kdb_event_worker_thread(self, appque, log, std_in): 356 | while self.kdb_thread_active: 357 | self.active = True 358 | cmd = "" 359 | try: 360 | # cmd = input() 361 | # with open(std_in) as inp: 362 | cmd = std_in.readline().strip() 363 | except Exception as e: 364 | log.info("Exception in input() {}".format(e)) 365 | time.sleep(1.0) 366 | if cmd == "": 367 | continue 368 | log.debug("keyboard: <{}>".format(cmd)) 369 | if len(cmd) >= 1: 370 | if cmd in self.kbd_moves: 371 | self.kbd_moves = [] 372 | appque.put( 373 | {'cmd': 'move', 'uci': cmd, 'actor': self.name}) 374 | elif cmd == '--': 375 | self.kbd_moves = [] 376 | appque.put( 377 | {'cmd': 'move', 'uci': '0000', 'actor': self.name}) 378 | elif cmd == 'a': 379 | log.debug('analyse') 380 | appque.put({'cmd': 'analyse', 'actor': self.name}) 381 | elif cmd == 'b': 382 | log.debug('move back') 383 | appque.put({'cmd': 'move_back', 'actor': self.name}) 384 | elif cmd == 'c': 385 | log.debug('change ChessLink board orientation') 386 | appque.put( 387 | {'cmd': 'turn_hardware_board', 'actor': self.name}) 388 | # elif cmd == 'e': 389 | # log.debug('board encoding switch') 390 | # appque.put({'encoding': '', 'actor': self.name}) 391 | elif cmd == 'f': 392 | log.debug('move forward') 393 | appque.put({'cmd': 'move_forward', 'actor': self.name}) 394 | elif cmd[:4] == 'fen ': 395 | appque.put( 396 | {'cmd': 'import_fen', 'fen': cmd[4:], 'actor': self.name}) 397 | elif cmd == 'g': 398 | log.debug('go') 399 | appque.put({'cmd': 'go', 'actor': self.name}) 400 | elif cmd[:2] == 'h ': 401 | log.debug( 402 | 'show analysis for n plies (max 4) on ChessLink board.') 403 | ply = int(cmd[2:]) 404 | if ply < 0: 405 | ply = 0 406 | if ply > 4: 407 | ply = 4 408 | appque.put({'cmd': 'led_info', 'plies': ply}) 409 | elif cmd[:1] == 'm': 410 | if len(cmd) == 4: 411 | if cmd[2:] == "PP": 412 | log.debug("mode: player-player") 413 | appque.put( 414 | {'cmd': 'game_mode', 'mode': 'human-human'}) 415 | elif cmd[2:] == "PE": 416 | log.debug("mode: player-engine") 417 | appque.put( 418 | {'cmd': 'game_mode', 'mode': 'human-computer'}) 419 | elif cmd[2:] == "EP": 420 | log.debug("mode: engine-player") 421 | appque.put( 422 | {'cmd': 'game_mode', 'mode': 'computer-human'}) 423 | elif cmd[2:] == "EE": 424 | log.debug("mode: engine-engine") 425 | appque.put( 426 | {'cmd': 'game_mode', 'mode': 'computer-computer'}) 427 | else: 428 | log.warning( 429 | 'Illegal m parameter, use: PP, PE, EP, EE (see help-command)') 430 | elif cmd == 'n': 431 | log.debug('requesting new game') 432 | appque.put({'cmd': 'new_game', 'actor': self.name}) 433 | elif cmd == 'p': 434 | log.debug('position_fetch') 435 | appque.put( 436 | {'cmd': 'position_fetch', 'from': 'ChessLinkAgent', 'actor': self.name}) 437 | elif cmd == 'q': 438 | appque.put({'cmd': 'quit', 'actor': self.name}) 439 | elif cmd == 's': 440 | log.debug('stop') 441 | appque.put({'cmd': 'stop', 'actor': self.name}) 442 | elif cmd == 'tw': 443 | log.debug('turn white') 444 | appque.put( 445 | {'cmd': 'turn', 'color': 'white', 'actor': self.name}) 446 | elif cmd == 'tb': 447 | log.debug('turn black') 448 | appque.put( 449 | {'cmd': 'turn', 'color': 'black', 'actor': self.name}) 450 | 451 | elif cmd == 'help': 452 | print('Terminal commands:') 453 | print('e2e4 - enter a valid move (in UCI format)') 454 | print('-- null move') 455 | print('a - analyse current position') 456 | print('b - take back move') 457 | print( 458 | 'c - change cable orientation (eboard cable left/right') 459 | print("fen - set board to position") 460 | print( 461 | 'g - go, current player (default white) or force current move') 462 | print('h - show hints for levels on board') 463 | print("m < mode > - modes: PP: Player-Player, PE: Player-Engine, ") 464 | print(" EP: Engine-Player, EE: Engine1-Engine2.") 465 | print('n - new game') 466 | print('p - import ChessLink board position') 467 | print('q - quit') 468 | print('s - stop and discard calculation') 469 | print('tw - next move: white') 470 | print('tb - next move: black') 471 | else: 472 | print( 473 | 'Unknown keyboard cmd <{}>, enter "help" for a list of valid commands.'.format(cmd)) 474 | 475 | def keyboard_handler(self): 476 | self.kdb_thread_active = True 477 | self.kbd_event_thread = threading.Thread( 478 | target=self.kdb_event_worker_thread, args=(self.appque, self.log, sys.stdin)) 479 | self.kbd_event_thread.setDaemon(True) 480 | self.kbd_event_thread.start() 481 | -------------------------------------------------------------------------------- /mchess/tk_agent.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import threading 4 | import copy 5 | import os 6 | 7 | import chess 8 | import chess.pgn 9 | 10 | # from tkinter import * 11 | # from tkinter.ttk import * 12 | # from tkinter import filedialog, font 13 | 14 | import tkinter as tk 15 | import tkinter.ttk as ttk 16 | from tkinter import font, filedialog 17 | 18 | import PIL 19 | from PIL import ImageTk, Image, ImageOps 20 | 21 | # By en:User:Cburnett - File:Chess klt45.svg, CC BY-SA 3.0, 22 | # https://commons.wikimedia.org/w/index.php?curid=20363779 23 | # https://commons.wikimedia.org/wiki/Template:SVG_chess_pieces 24 | # convert -background none -density 128 -resize 128x Chess_bdt45.svg cbd.gif 25 | 26 | 27 | class GameBoard(ttk.Frame): 28 | def __init__(self, parent, size=64, r=0, c=0, color1="white", color2="gray", 29 | bg_color="black", ol_color="black", log=None): 30 | '''size is the size of a square, in pixels''' 31 | 32 | self.rows = 8 33 | self.log = log 34 | self.columns = 8 35 | self.size = size 36 | self.color1 = color1 37 | self.color2 = color2 38 | self.bg_color = bg_color 39 | self.ol_color = ol_color 40 | self.height = None 41 | self.width = None 42 | self.pieces = {} 43 | self.figrep = {"png60": ["wp60.png", "wn60.png", "wb60.png", "wr60.png", "wq60.png", 44 | "wk60.png", "bp60.png", "bn60.png", "bb60.png", "br60.png", 45 | "bq60.png", "bk60.png"]} 46 | self.position = [] 47 | self.valid_move_list = [] 48 | self.move_part = 0 49 | self.move_actor = None 50 | self.cur_move = "" 51 | 52 | for _ in range(8): 53 | row = [] 54 | for _ in range(8): 55 | row.append(-1) 56 | self.position.append(row) 57 | 58 | canvas_width = self.columns * size 59 | canvas_height = self.rows * size 60 | 61 | ttk.Frame.__init__(self, parent) 62 | self.canvas = tk.Canvas(parent, borderwidth=0, highlightthickness=0, 63 | width=canvas_width, height=canvas_height, background=bg_color) 64 | self.canvas.grid(row=r, column=c, sticky="news") 65 | # self.canvas.grid_columnconfigure(0, weight=1) 66 | # self.canvas.grid_rowconfigure(0, weight=1) 67 | self.load_figures(size) 68 | self.canvas.bind("", self.refresh) 69 | self.canvas.bind("", self.mouse_click) 70 | 71 | def load_figures(self, size): 72 | self.png60s = [] 73 | img_size = size-4 74 | for fn in self.figrep['png60']: 75 | fp = os.path.join('resources/pieces', fn) 76 | img = Image.open(fp).convert('RGBA').resize( 77 | (img_size, img_size), Image.ANTIALIAS) 78 | self.png60s.append(ImageTk.PhotoImage(img)) 79 | 80 | def mouse_click(self, event): 81 | x = chr(event.x//self.size+ord('a')) 82 | y = chr(7-(event.y//self.size)+ord('1')) 83 | if self.move_part == 0: 84 | cc = f"{x}{y}" 85 | self.cur_move = "" 86 | else: 87 | cc = f"{self.cur_move}{x}{y}" 88 | if len(self.valid_move_list) > 0: 89 | f = [] 90 | for mv in self.valid_move_list: 91 | if mv[0:self.move_part*2+2] == cc: 92 | f.append(mv) 93 | if len(f) > 0: 94 | if self.move_part == 0: 95 | self.cur_move = cc 96 | self.move_part += 1 97 | return 98 | else: 99 | if len(f) > 1 and self.log is not None: 100 | self.log.error("This is non-implemented situation") 101 | # XXX: select pawn upgrade GUI 102 | self.move_actor(f[0]) 103 | else: 104 | if self.log is not None: 105 | self.log.warning("Invalid entry!") 106 | self.move_part = 0 107 | self.cur_move = "" 108 | else: 109 | if self.log is not None: 110 | self.log.warning( 111 | "You are not allowed to click on the board at this time!") 112 | self.move_part = 0 113 | self.cur_move = 0 114 | 115 | print(f"Click at {cc}") 116 | 117 | def register_moves(self, move_list, move_actor=None): 118 | print(move_list) 119 | self.move_actor = move_actor 120 | self.move_part = 0 121 | self.valid_move_list = move_list 122 | 123 | def refresh(self, event=None): 124 | redraw_fields = False 125 | if event is not None: 126 | if self.height != event.height or self.width != event.width: 127 | redraw_fields = True 128 | self.width = event.width 129 | self.height = event.height 130 | # Redraw the board, possibly in response to window being resized 131 | xsize = int((self.width-1) / self.columns) 132 | ysize = int((self.height-1) / self.rows) 133 | self.size = min(xsize, ysize) 134 | self.load_figures(self.size) 135 | 136 | if redraw_fields is True: 137 | self.canvas.delete("square") 138 | self.canvas.delete("piece") 139 | color = self.color2 140 | for row in range(self.rows): 141 | color = self.color1 if color == self.color2 else self.color2 142 | for col in range(self.columns): 143 | x1 = (col * self.size) 144 | y1 = (row * self.size) 145 | x2 = x1 + self.size 146 | y2 = y1 + self.size 147 | if redraw_fields is True: 148 | self.canvas.create_rectangle(x1, y1, x2, y2, outline=self.ol_color, 149 | fill=color, tags="square") 150 | color = self.color1 if color == self.color2 else self.color2 151 | img_ind = self.position[row][col] 152 | if img_ind != -1: 153 | self.canvas.create_image(x1, y1, image=self.png60s[img_ind], 154 | tags=("piece"), anchor="nw") 155 | self.canvas.tag_raise("piece") 156 | self.canvas.tag_lower("square") 157 | 158 | 159 | class TkAgent: 160 | def __init__(self, appque, prefs): 161 | self.figrep = {"int": [1, 2, 3, 4, 5, 6, 0, -1, -2, -3, -4, -5, -6], 162 | "pythc": [(chess.PAWN, chess.WHITE), (chess.KNIGHT, chess.WHITE), 163 | (chess.BISHOP, chess.WHITE), (chess.ROOK, chess.WHITE), 164 | (chess.QUEEN, chess.WHITE), (chess.KING, chess.WHITE), 165 | (chess.PAWN, chess.BLACK), (chess.KNIGHT, chess.BLACK), 166 | (chess.BISHOP, chess.BLACK), (chess.ROOK, chess.BLACK), 167 | (chess.QUEEN, chess.BLACK), (chess.KING, chess.BLACK)], 168 | "unic": "♟♞♝♜♛♚ ♙♘♗♖♕♔", 169 | "png60": ["wp60.png", "wn60.png", "wb60.png", "wr60.png", "wq60.png", 170 | "wk60.png", "bp60.png", "bn60.png", "bb60.png", "br60.png", 171 | "bq60.png", "bk60.png"], 172 | "ascii": "PNBRQK.pnbrqk"} 173 | self.turquoise = { 174 | "light": "#D8DBE2", # Gainsboro 175 | "dlight": "#A9BCC0", # Pastel Blue 176 | "turquoise": "#58A4B0", # Cadet Blue 177 | "silver": "#C0C0C0", # Silver 178 | "darkgray": "#A9A9A9", # Darkgray 179 | "ldark": "#373F41", # Charcoil 180 | "dark": "#2E3532", # Jet 181 | "ddark": "#282A32", # Charleston Green 182 | "dddark": "#1B1B1E", # Eerie Black 183 | "xdddark": "#202022", # X Black 184 | } 185 | self.chesssym = {"unic": ["-", "×", "†", "‡", "½"], 186 | "ascii": ["-", "x", "+", "#", "1/2"]} 187 | 188 | self.name = 'TkAgent' 189 | self.prefs = prefs 190 | self.log = logging.getLogger("TkAgent") 191 | self.appque = appque 192 | self.orientation = True 193 | self.active = False 194 | self.agent_state_cache = {} 195 | self.tk_moves = [] 196 | self.png60s = None 197 | self.title_text = None 198 | 199 | self.board = None 200 | self.tk_board = None 201 | self.tk_board2 = None 202 | self.title = None 203 | self.movelist = None 204 | self.analist = None 205 | self.gui_init = False 206 | 207 | self.tkapp_thread_active = True 208 | 209 | self.tkapp_thread = threading.Thread( 210 | target=self.tkapp_worker_thread, args=(self.appque, self.log)) 211 | self.tkapp_thread.setDaemon(True) 212 | self.tkapp_thread.start() 213 | 214 | t0 = time.time() 215 | warned = False 216 | while self.gui_init is False: 217 | time.sleep(0.1) 218 | if time.time()-t0 > 2 and warned is False: 219 | warned = True 220 | self.log.error("Tk GUI is not responding in time!") 221 | if time.time()-t0 > 5: 222 | return 223 | self.log.info("GUI online.") 224 | self.active = True 225 | 226 | def agent_ready(self): 227 | return self.active 228 | 229 | def quit(self): 230 | self.tkapp_thread_active = False 231 | 232 | def board2pos(self, board): 233 | pos = [] 234 | for y in reversed(range(8)): 235 | row = [] 236 | for x in range(8): 237 | fig = board.piece_at(chess.square(x, y)) 238 | if fig is not None: 239 | ind = 0 240 | for f0 in self.figrep['pythc']: 241 | if fig.piece_type == f0[0] and fig.color == f0[1]: 242 | break 243 | ind += 1 244 | if ind < len(self.figrep['pythc']): 245 | row.append(ind) 246 | else: 247 | row.append(-1) 248 | self.log.error(f'Figure conversion error at {x}{y}') 249 | else: 250 | row.append(-1) 251 | pos.append(row) 252 | return pos 253 | 254 | def display_board(self, board, attribs={'unicode': True, 'invert': False, 255 | 'white_name': 'white', 'black_name': 'black'}): 256 | self.log.info("display_board") 257 | if self.gui_init is False: 258 | return 259 | self.title_text.set(attribs["white_name"] + 260 | " - " + attribs["black_name"]) 261 | self.tk_board.position = self.board2pos(board) 262 | self.tk_board.refresh() 263 | 264 | try: 265 | game = chess.pgn.Game().from_board(board) 266 | game.headers["White"] = attribs["white_name"] 267 | game.headers["Black"] = attribs["black_name"] 268 | pgntxt = str(game) 269 | pgntxt = ''.join(pgntxt.splitlines()[8:]) 270 | except Exception as e: 271 | self.log.error(f"Invalid PGN position, {e}") 272 | return 273 | self.movelist.delete("1.0", tk.END) 274 | self.movelist.insert("1.0", pgntxt) 275 | 276 | def display_move(self, move_msg): 277 | pass 278 | 279 | def display_info(self, board, info, max_board_preview_hmoves=6): 280 | # if info['multipv_ind'] != 1: 281 | # return 282 | mpv_ind = info['multipv_ind'] 283 | ninfo = copy.deepcopy(info) 284 | nboard = copy.deepcopy(board) 285 | nboard_cut = copy.deepcopy(nboard) 286 | max_cut = max_board_preview_hmoves 287 | if 'variant' in ninfo: 288 | ml = [] 289 | mv = '' 290 | if nboard.turn is False: 291 | mv = (nboard.fullmove_number,) 292 | mv += ("..",) 293 | rel_mv = 0 294 | for move in ninfo['variant']: 295 | if move is None: 296 | self.log.error(f"None-move in variant: {ninfo}") 297 | if nboard.turn is True: 298 | mv = (nboard.fullmove_number,) 299 | try: 300 | san = nboard.san(move) 301 | except Exception as e: 302 | self.log.warning( 303 | f"Internal error '{e}' at san conversion.") 304 | san = None 305 | if san is not None: 306 | mv += (san,) 307 | else: 308 | self.log.info( 309 | f"Variant cut off due to san-conversion-error: '{mv}'") 310 | break 311 | if nboard.turn is False: 312 | ml.append(mv) 313 | mv = "" 314 | nboard.push(move) 315 | if rel_mv < max_cut: 316 | nboard_cut.push(move) 317 | rel_mv += 1 318 | if mv != "": 319 | ml.append(mv) 320 | mv = "" 321 | ninfo['variant'] = ml 322 | self.analist.delete(f"{mpv_ind}.0", f"{mpv_ind+1}.0") 323 | self.analist.insert(f"{mpv_ind}.0", f"[{mpv_ind}]: " + str(ml) + "\n") 324 | if mpv_ind == 1: 325 | self.tk_board2.position = self.board2pos(nboard_cut) 326 | self.tk_board2.refresh() 327 | 328 | def agent_states(self, msg): 329 | self.agent_state_cache[msg['actor']] = msg 330 | 331 | def do_move(self, move): 332 | self.appque.put({'move': {'uci': move, 'actor': self.name}}) 333 | 334 | def set_valid_moves(self, board, vals): 335 | tk_moves = [] 336 | self.board = board 337 | if vals is not None: 338 | for v in vals: 339 | tk_moves.append(vals[v]) 340 | self.tk_board.register_moves(tk_moves, self.do_move) 341 | 342 | def tkapp_worker_thread(self, appque, log): 343 | root = tk.Tk() 344 | default_font = font.nametofont("TkDefaultFont") 345 | default_font.configure(size=10) 346 | text_font = font.nametofont("TkTextFont") 347 | text_font.configure(size=10) 348 | fixed_font = font.nametofont("TkFixedFont") 349 | fixed_font.configure(size=10) 350 | # self.frame = Frame(root) 351 | for i in range(3): 352 | tk.Grid.columnconfigure(root, i, weight=1) 353 | # if i>0: 354 | tk.Grid.rowconfigure(root, i, weight=1) 355 | # for i in range(3): 356 | # Grid.columnconfigure(self.frame, i, weight=1) 357 | # Grid.rowconfigure(self.frame, i, weight=1) 358 | # self.frame.grid(sticky=N+S+W+E) 359 | 360 | self.bof = ttk.Frame(root) 361 | for i in range(3): 362 | tk.Grid.columnconfigure(self.bof, i, weight=1) 363 | # if i>0: 364 | tk.Grid.rowconfigure(self.bof, i, weight=1) 365 | 366 | self.bof.grid(row=1, column=0, sticky="news") 367 | self.tk_board = GameBoard(self.bof, log=self.log, r=1, c=0, 368 | color1=self.turquoise['dlight'], 369 | color2=self.turquoise['turquoise'], 370 | bg_color=self.turquoise['ldark'], 371 | ol_color=self.turquoise['darkgray']) 372 | self.tk_board.grid(row=1, column=0, sticky="news") 373 | 374 | s = 20 375 | self.bfr = ttk.Frame(self.bof) 376 | self.bfr.grid(row=2, column=0, sticky="news") 377 | img = Image.open( 378 | 'web/images/bb.png').convert('RGBA').resize((s, s), Image.ANTIALIAS) 379 | bbackimg = ImageTk.PhotoImage(img) 380 | self.button_bback = ttk.Button( 381 | self.bfr, image=bbackimg, command=self.on_fast_back) 382 | # background=self.turquoise['dlight'], , relief=FLAT) 383 | # self.button_bback.configure(padx=15, pady=15) 384 | self.button_bback.grid( 385 | row=0, column=0, sticky="ew", padx=(5, 5), pady=(7, 7)) 386 | img = Image.open( 387 | 'web/images/b.png').convert('RGBA').resize((s, s), Image.ANTIALIAS) 388 | backimg = ImageTk.PhotoImage(img) 389 | self.button_back = ttk.Button( 390 | self.bfr, image=backimg, command=self.on_back) 391 | # , relief=FLAT) 392 | self.button_back.grid(row=0, column=1, sticky="ew", 393 | padx=(5, 5), pady=(7, 7)) 394 | img = Image.open( 395 | 'web/images/stop.png').convert('RGBA').resize((s, s), Image.ANTIALIAS) 396 | stopimg = ImageTk.PhotoImage(img) 397 | self.button_stop = ttk.Button( 398 | self.bfr, image=stopimg, command=self.on_stop) 399 | # , relief=FLAT) 400 | self.button_stop.grid(row=0, column=2, sticky="ew", 401 | padx=(8, 8), pady=(7, 7)) 402 | img = Image.open( 403 | 'web/images/f.png').convert('RGBA').resize((s, s), Image.ANTIALIAS) 404 | forimg = ImageTk.PhotoImage(img) 405 | self.button_forward = ttk.Button( 406 | self.bfr, image=forimg, command=self.on_forward) 407 | # , relief=FLAT) 408 | self.button_forward.grid( 409 | row=0, column=3, sticky="ew", padx=(5, 5), pady=(7, 7)) 410 | img = Image.open( 411 | 'web/images/ff.png').convert('RGBA').resize((s, s), Image.ANTIALIAS) 412 | fforimg = ImageTk.PhotoImage(img) 413 | self.button_fforward = ttk.Button( 414 | self.bfr, image=fforimg, command=self.on_fast_forward) 415 | # , relief=FLAT) 416 | self.button_fforward.grid( 417 | row=0, column=4, sticky="ew", padx=(5, 5), pady=(7, 7)) 418 | 419 | self.tk_board2 = GameBoard(root, log=self.log, r=1, c=2, color1=self.turquoise['dlight'], 420 | color2=self.turquoise['turquoise'], 421 | bg_color=self.turquoise['ldark'], 422 | ol_color=self.turquoise['darkgray']) 423 | self.movelist = tk.Text(root) 424 | self.analist = tk.Text(root, height=10) 425 | self.title_text = tk.StringVar() 426 | self.title = ttk.Label(root, textvariable=self.title_text) 427 | 428 | self.title.grid(row=0, column=0, sticky="ew") 429 | self.movelist.grid(row=1, column=1, sticky="news") 430 | self.tk_board2.grid(row=1, column=2, sticky="news") 431 | self.analist.grid(row=2, column=2, sticky="ew") 432 | 433 | menubar = tk.Menu(root) 434 | root.config(menu=menubar) 435 | 436 | file_menu = tk.Menu(menubar, tearoff=0) 437 | file_menu.add_command(label="New Game", command=self.on_new, underline=0, 438 | accelerator="Ctrl+n") 439 | root.bind_all("", self.on_new) 440 | file_menu.add_separator() 441 | file_menu.add_command(label="Open PGN file...", command=self.on_pgn_open, underline=0, 442 | accelerator="Ctrl+o") 443 | root.bind_all("", self.on_pgn_open) 444 | file_menu.add_command(label="Save PGN file...", command=self.on_pgn_save, underline=0, 445 | accelerator="Ctrl+s") 446 | root.bind_all("", self.on_pgn_save) 447 | file_menu.add_separator() 448 | file_menu.add_command(label="Exit", command=self.on_exit, underline=1, 449 | accelerator="Ctrl+x") 450 | root.bind_all("", self.on_exit) 451 | 452 | game_menu = tk.Menu(menubar, tearoff=0) 453 | 454 | submenu = tk.Menu(game_menu) 455 | submenu.add_command(label="Player - Player", command=self.on_mode_pp) 456 | submenu.add_command(label="Player - Engine", command=self.on_mode_pe) 457 | submenu.add_command(label="Engine - Player", command=self.on_mode_ep) 458 | submenu.add_command(label="Engline - Engine", command=self.on_mode_ee) 459 | game_menu.add_cascade(label="Game mode", menu=submenu, underline=6) 460 | 461 | game_menu.add_separator() 462 | game_menu.add_command(label="Go", command=self.on_go, 463 | underline=0, accelerator="Ctrl+g") 464 | root.bind_all("", self.on_go) 465 | game_menu.add_command( 466 | label="Beginning", command=self.on_fast_back, underline=0) 467 | game_menu.add_command(label="Back", command=self.on_back, underline=0, 468 | accelerator="Ctrl+b") 469 | root.bind_all("", self.on_back) 470 | game_menu.add_command(label="Forward", command=self.on_forward, underline=0, 471 | accelerator="Ctrl+f") 472 | root.bind_all("", self.on_forward) 473 | game_menu.add_command( 474 | label="End", command=self.on_fast_forward, underline=0) 475 | game_menu.add_separator() 476 | game_menu.add_command(label="Stop", command=self.on_stop, underline=1, 477 | accelerator="Ctrl+t") 478 | root.bind_all("", self.on_stop) 479 | game_menu.add_separator() 480 | game_menu.add_command(label="Analyse", command=self.on_analyse, underline=0, 481 | accelerator="Ctrl+a") 482 | root.bind_all("", self.on_analyse) 483 | 484 | menubar.add_cascade(label="File", menu=file_menu, underline=0) 485 | menubar.add_cascade(label="Game", menu=game_menu, underline=0) 486 | 487 | self.gui_init = True 488 | root.mainloop() 489 | 490 | def on_new(self, event=None): 491 | self.appque.put({'new game': '', 'actor': self.name}) 492 | 493 | def on_go(self, event=None): 494 | self.appque.put({'go': 'current', 'actor': self.name}) 495 | 496 | def on_back(self, event=None): 497 | self.appque.put({'back': '', 'actor': self.name}) 498 | 499 | def on_fast_back(self, event=None): 500 | self.appque.put({'fast-back': '', 'actor': self.name}) 501 | 502 | def on_forward(self, event=None): 503 | self.appque.put({'forward': '', 'actor': self.name}) 504 | 505 | def on_fast_forward(self, event=None): 506 | self.appque.put({'fast-forward': '', 'actor': self.name}) 507 | 508 | def on_stop(self, event=None): 509 | self.appque.put({'stop': '', 'actor': self.name}) 510 | 511 | def on_analyse(self, event=None): 512 | self.appque.put({'analysis': '', 'actor': self.name}) 513 | 514 | def on_exit(self, event=None): 515 | self.appque.put({'quit': '', 'actor': self.name}) 516 | 517 | def on_mode_pp(self, event=None): 518 | self.appque.put({'game_mode': 'PLAYER_PLAYER'}) 519 | 520 | def on_mode_pe(self, event=None): 521 | self.appque.put({'game_mode': 'PLAYER_ENGINE'}) 522 | 523 | def on_mode_ep(self, event=None): 524 | self.appque.put({'game_mode': 'ENGINE_PLAYER'}) 525 | 526 | def on_mode_ee(self, event=None): 527 | self.appque.put({'game_mode': 'ENGINE_ENGINE'}) 528 | 529 | def load_pgns(self, fn): 530 | try: 531 | with open(fn, 'r') as f: 532 | d = f.read() 533 | except Exception as e: 534 | print(f"Failed to read {fn}: {e}") 535 | return None 536 | pt = d.split('\n\n') 537 | if len(pt) % 2 != 0: 538 | print("Bad structure or incomplete!") 539 | return None 540 | if len(pt) == 0: 541 | print("Empty") 542 | return None 543 | games = [] 544 | for i in range(0, len(pt), 2): 545 | gi = pt[i]+"\n\n"+pt[i+1] 546 | games.append(gi) 547 | return games 548 | 549 | def on_pgn_open(self, event=None): 550 | filename = filedialog.askopenfilename(initialdir=".", title="Select PGN file", 551 | filetypes=(("pgn files", "*.pgn"), 552 | ("all files", "*.*"))) 553 | games = self.load_pgns(filename) 554 | if len(games) > 1: 555 | self.log.warning( 556 | f'File contained {len(games)}, only first game read.') 557 | if games is not None: 558 | self.appque.put({'pgn_game': {'pgn_data': games[0]}}) 559 | 560 | def on_pgn_save(self, event=None): 561 | filename = filedialog.asksaveasfilename(initialdir=".", 562 | title="Select PGN file", 563 | filetypes=(("pgn files", "*.pgn"), 564 | ("all files", "*.*"))) 565 | print(filename) 566 | -------------------------------------------------------------------------------- /mchess/turquoise.py: -------------------------------------------------------------------------------- 1 | ''' Turquoise chess main module ''' 2 | import argparse 3 | import logging 4 | import json 5 | import importlib 6 | import queue 7 | 8 | from turquoise_dispatch import TurquoiseDispatcher 9 | 10 | 11 | __version__ = "0.4.1" 12 | 13 | 14 | class TurquoiseSetup(): 15 | ''' Load configuration and prepare agent initialization ''' 16 | 17 | def __init__(self, args): 18 | self.args = args 19 | self.preference_version = 1 20 | 21 | # Entries: 'config_name': ('module_name', 'class(es)') 22 | self.known_agents = { 23 | 'chesslink': ('chess_link_agent', 'ChessLinkAgent'), 24 | 'terminal': ('terminal_agent', 'TerminalAgent'), 25 | 'tk': ('tk_agent', 'TkAgent'), 26 | 'qt': ('qt_agent', 'QtAgent'), 27 | 'web': ('async_web_agent', 'AsyncWebAgent'), 28 | 'computer': ('async_uci_agent', ['UciEngines', 'UciAgent']) 29 | } 30 | 31 | self.log = logging.getLogger("TurquoiseStartup") 32 | 33 | # self.imports = {'Darwin': ['chess_link_usb'], 'Linux': [ 34 | # 'chess_link_bluepy', 'chess_link_usb'], 'Windows': ['chess_link_usb']} 35 | # system = platform.system() 36 | 37 | self.prefs = self.read_preferences(self.preference_version) 38 | self.config_logging(self.prefs) 39 | 40 | self.main_thread = None 41 | self.dispatcher = None 42 | self.main_event_queue = queue.Queue() 43 | 44 | self.agent_modules = {} 45 | self.uci_engine_configurator = None 46 | self.agents = {} 47 | self.engines = {} 48 | for agent in self.known_agents: 49 | if agent in self.prefs['agents']: 50 | try: 51 | module = importlib.import_module( 52 | self.known_agents[agent][0]) 53 | self.agent_modules[agent] = module 54 | except Exception as e: 55 | self.log.error( 56 | f"Failed to import module {self.known_agents[agent][0]} for agent {agent}: {e}") 57 | 58 | def write_preferences(self, pref): 59 | try: 60 | with open("preferences.json", "w") as fp: 61 | json.dump(pref, fp, indent=4) 62 | except Exception as e: 63 | self.log.error(f"Failed to write preferences.json, {e}") 64 | 65 | def set_default_preferences(self, version): 66 | prefs = { 67 | "version": version, 68 | "agents": ["chesslink", "terminal", "web", "computer"], 69 | "default_human_player": { 70 | "name": "human", 71 | "location": "" 72 | }, 73 | "chesslink": { 74 | "max_plies_board": 3, 75 | "ply_vis_delay": 80, 76 | "import_position": True, 77 | "autodetect": True, 78 | "orientation": True, 79 | "btle_iface": 0, 80 | "protocol_debug": False, 81 | "bluetooth_address": "", 82 | "usb_port": "", 83 | "transport": "" 84 | }, 85 | "terminal": { 86 | "use_unicode_figures": True, 87 | "invert_term_color": False, 88 | "max_plies_terminal": 10 89 | }, 90 | "web": { 91 | "port": 8001, 92 | "bind_address": "localhost", 93 | "tls": False, 94 | "private_key": "", 95 | "public_key": "" 96 | }, 97 | "tk": { 98 | "main_thread": False 99 | }, 100 | "qt": { 101 | "main_thread": True 102 | }, 103 | "computer": { 104 | "think_ms": 500, 105 | "default_player": "stockfish", 106 | "default_2nd_analyser": "lc0", 107 | "engines": [ 108 | "stockfish" 109 | ] 110 | }, 111 | "log_levels": { 112 | "chess.engine": "ERROR" 113 | } 114 | } 115 | return prefs 116 | 117 | def read_preferences(self, version): 118 | prefs = {} 119 | default_prefs = False 120 | try: 121 | with open('preferences.json', 'r') as f: 122 | prefs = json.load(f) 123 | except Exception as e: 124 | default_prefs = True 125 | self.log.warning( 126 | f'Failed to read preferences.json: {e}') 127 | if 'version' not in prefs: 128 | default_prefs = True 129 | else: 130 | if prefs['version'] < version: 131 | self.log.warning( 132 | 'preferences.json file is outdated, initializing default values.') 133 | default_prefs = True 134 | 135 | if default_prefs is True: 136 | prefs = self.set_default_preferences(version) 137 | self.write_preferences(prefs) 138 | return prefs 139 | 140 | def config_logging(self, prefs): 141 | if 'log_levels' in prefs: 142 | for module in prefs['log_levels']: 143 | level = logging.getLevelName(prefs['log_levels'][module]) 144 | logi = logging.getLogger(module) 145 | logi.setLevel(level) 146 | else: 147 | self.log.warning('Custom log levels not defined') 148 | 149 | def main(self): 150 | for agent in self.agent_modules: 151 | class_name = self.known_agents[agent][1] 152 | if isinstance(class_name, list): 153 | if class_name[0] == 'UciEngines': 154 | self.uci_engine_configurator = self.agent_modules[agent].UciEngines( 155 | self.main_event_queue, self.prefs[agent]) 156 | for engine in self.uci_engine_configurator.engines: 157 | self.log.info(f"Found engine {engine}") 158 | engine_json = self.uci_engine_configurator.engines[engine]['params'] 159 | if engine == self.prefs['computer']['default_player']: 160 | self.log.info(f"{engine} is default-engine") 161 | self.agents['uci1'] = self.agent_modules[agent].UciAgent( 162 | self.main_event_queue, engine_json, self.prefs['computer']) 163 | if self.agents['uci1'] is None: 164 | self.log.error( 165 | f'Failed to instantiate {engine}') 166 | if engine == self.prefs['computer']['default_2nd_analyser']: 167 | self.log.info(f"{engine} is 2nd-engine") 168 | self.agents['uci2'] = self.agent_modules[agent].UciAgent( 169 | self.main_event_queue, engine_json, self.prefs['computer']) 170 | if self.agents['uci2'] is None: 171 | self.log.error( 172 | f'Failed to instantiate {engine}') 173 | # XXX: startup 1..n engine-agents ?! 174 | else: 175 | self.log.error(f"Not yet implemented: {class_name}") 176 | else: 177 | try: 178 | self.log.info(f"Instantiating agent {agent}, {class_name}") 179 | agent_class = getattr( 180 | self.agent_modules[agent], class_name) 181 | self.agents[agent] = agent_class( 182 | self.main_event_queue, self.prefs[agent]) 183 | except Exception as e: 184 | self.log.error( 185 | f"Failed to instantiate {class_name} for agent {agent}: {e}") 186 | 187 | # mainthreader id 188 | self.dispatcher = TurquoiseDispatcher( 189 | self.main_event_queue, self.prefs, self.agents, self.uci_engine_configurator) 190 | 191 | try: 192 | self.dispatcher.game_state_machine_NEH() 193 | except KeyboardInterrupt: 194 | self.dispatcher.quit() 195 | 196 | 197 | if __name__ == '__main__': 198 | parser = argparse.ArgumentParser(prog='python mchess.py') 199 | parser.add_argument('-v', '--verbose', action='store_true', 200 | help='output verbose logging information') 201 | args = parser.parse_args() 202 | 203 | msg = r""" 204 | _______ _ 205 | |__ __| (_) 206 | | |_ _ _ __ __ _ _ _ ___ _ ___ ___ 207 | | | | | | '__/ _` | | | |/ _ \| / __|/ _ \ 208 | | | |_| | | | (_| | |_| | (_) | \__ \ __/ 209 | |_|\__,_|_| \__, |\__,_|\___/|_|___/\___| 210 | | | _____ _ {} 211 | |_| / ____| | 212 | _ __ ___ | | | |__ ___ ___ ___ 213 | | '_ ` _ \| | | '_ \ / _ \/ __/ __| 214 | | | | | | | |____| | | | __/\__ \__ \\ 215 | |_| |_| |_|\_____|_| |_|\___||___/___/""" 216 | print(msg.format(__version__)) 217 | print(" Enter 'help' to see an overview of console commands") 218 | if args.verbose is True: 219 | log_level = logging.DEBUG 220 | else: 221 | log_level = logging.INFO 222 | 223 | logging.basicConfig(format='%(asctime)s %(levelname)s %(name)s %(message)s', 224 | level=log_level, filename='turquoise.log', filemode='w') 225 | 226 | # console = logging.StreamHandler() 227 | # console.setLevel(logging.INFO) 228 | 229 | logger = logging.getLogger('Turquoise') 230 | 231 | logger.setLevel(log_level) 232 | logger.info("---------------------------------") 233 | logger.info("STARTING") 234 | logger.setLevel(log_level) 235 | 236 | ts = TurquoiseSetup(args) 237 | ts.main() 238 | -------------------------------------------------------------------------------- /mchess/web/clean_inst.sh: -------------------------------------------------------------------------------- 1 | npm install --save cm-chessboard 2 | 3 | -------------------------------------------------------------------------------- /mchess/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/favicon.ico -------------------------------------------------------------------------------- /mchess/web/images/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/b.png -------------------------------------------------------------------------------- /mchess/web/images/bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/bb.png -------------------------------------------------------------------------------- /mchess/web/images/bigmac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/bigmac.png -------------------------------------------------------------------------------- /mchess/web/images/btn-bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-bb.png -------------------------------------------------------------------------------- /mchess/web/images/btn-bk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-bk.png -------------------------------------------------------------------------------- /mchess/web/images/btn-bn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-bn.png -------------------------------------------------------------------------------- /mchess/web/images/btn-bp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-bp.png -------------------------------------------------------------------------------- /mchess/web/images/btn-bq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-bq.png -------------------------------------------------------------------------------- /mchess/web/images/btn-br.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-br.png -------------------------------------------------------------------------------- /mchess/web/images/btn-wb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-wb.png -------------------------------------------------------------------------------- /mchess/web/images/btn-wk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-wk.png -------------------------------------------------------------------------------- /mchess/web/images/btn-wn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-wn.png -------------------------------------------------------------------------------- /mchess/web/images/btn-wp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-wp.png -------------------------------------------------------------------------------- /mchess/web/images/btn-wq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-wq.png -------------------------------------------------------------------------------- /mchess/web/images/btn-wr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/btn-wr.png -------------------------------------------------------------------------------- /mchess/web/images/f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/f.png -------------------------------------------------------------------------------- /mchess/web/images/ff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/ff.png -------------------------------------------------------------------------------- /mchess/web/images/new_game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/new_game.png -------------------------------------------------------------------------------- /mchess/web/images/setup_position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/setup_position.png -------------------------------------------------------------------------------- /mchess/web/images/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/stop.png -------------------------------------------------------------------------------- /mchess/web/images/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/tree.png -------------------------------------------------------------------------------- /mchess/web/images/turquoise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/turquoise.png -------------------------------------------------------------------------------- /mchess/web/images/wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/mchess/web/images/wrench.png -------------------------------------------------------------------------------- /mchess/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 26 |
Turquoise chess v0.3.0
28 |
connected 30 | | ChessLink | |
33 |
34 | 35 | 43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 | 54 | 56 |  —  57 | 58 | 60 | 62 |
63 |
65 | 66 |
67 |
68 |
69 | 73 | 75 | 77 | 79 | 81 | 83 | 85 |
86 | 88 | 90 | 92 | 94 | 96 | 98 |
99 |
100 | 104 | 108 | 112 | 116 | 120 | 124 | 128 |
129 |
130 | FEN: 131 | 132 |  ↵  133 |
134 | 139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
148 |
149 | 150 |
151 |
152 |
153 | 159 | 161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
170 |
171 | 172 |
173 |
174 |
175 | 181 | 183 |
184 |
185 |
186 |
187 | 188 | 189 |
190 |
191 |
192 |
193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /mchess/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "dependencies": { 4 | "chart.js": "^2.9.3", 5 | "cm-chessboard": "^2.15.7" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /mchess/web/styles/mchess.css: -------------------------------------------------------------------------------- 1 | /* Mchess.css */ 2 | 3 | /* reset everything we need, obsoletes normalize.css */ 4 | html, 5 | body, 6 | div, 7 | span, 8 | iframe { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | font-size: 100%; 13 | font: Arial, Helvetica, sans-serif; 14 | vertical-align: baseline; 15 | } 16 | 17 | :root { 18 | --color-light: #D8DBE2; 19 | /*Gainsboro */ 20 | --color-dlight: #A9BCc0; 21 | /* Pastel Blue */ 22 | --color-turquoise: #58A4B0; 23 | /* Cadet Blue */ 24 | --color-ldark: #373F41; 25 | /* Charcoil */ 26 | --color-dark: #2E3532; 27 | /* Jet * */ 28 | --color-ddark: #282A32; 29 | /* Charleston Green */ 30 | --color-dddark: #1B1B1E; 31 | /* Eerie Black */ 32 | --color-xdddark: #202022; 33 | /* X Black */ 34 | } 35 | 36 | body { 37 | background: var(--color-dddark, #fff); 38 | color: var(--color-light) 39 | } 40 | 41 | 42 | .header { 43 | display: grid; 44 | grid-template-columns: 60px 400px; 45 | /* grid-template-rows: repeat(8, 1fr); */ 46 | grid-auto-rows: minmax(0px, auto); 47 | background: #222; 48 | } 49 | 50 | .logo { 51 | grid-column: 1/2; 52 | grid-row: 1/3; 53 | margin-top: auto; 54 | margin-bottom: auto; 55 | vertical-align: middle; 56 | padding: 5px; 57 | padding-left: 10px; 58 | } 59 | 60 | .title { 61 | /* background: #999; */ 62 | grid-column: 2/3; 63 | grid-row: 1/2; 64 | font-size: 12pt; 65 | line-height: 0.4; 66 | text-align: left; 67 | padding-top: 6px; 68 | } 69 | 70 | .subtitle { 71 | /* background: #888; */ 72 | font-size: 7pt; 73 | color: var(--color-dlight); 74 | grid-column: 2/3; 75 | grid-row: 2/3; 76 | text-align: left; 77 | vertical-align: top; 78 | } 79 | 80 | .state-light { 81 | vertical-align: 1pt; 82 | font-size: 7pt; 83 | } 84 | 85 | .engine-light { 86 | vertical-align: 1pt; 87 | font-size: 7pt; 88 | } 89 | 90 | .white { 91 | font-size: 12pt; 92 | vertical-align: -12pt; 93 | color: var(--color-light); 94 | } 95 | 96 | .turq { 97 | font-size: 12pt; 98 | vertical-align: -12pt; 99 | color: var(--color-turquoise); 100 | } 101 | 102 | .version { 103 | font-size: 7pt; 104 | color: var(--color-turquoise); 105 | padding-top: 6px; 106 | line-height: 0.4; 107 | vertical-align: -12pt; 108 | } 109 | 110 | .panel-header { 111 | background: var(--color-ddark); 112 | color: var(--color-dlight); 113 | font-size: 8pt; 114 | text-align: center; 115 | padding: 0px; 116 | /* text-align: center; */ 117 | } 118 | 119 | .panel-header-select { 120 | padding: 4px; 121 | /* text-align: center; */ 122 | -webkit-appearance: none; 123 | -moz-appearance: none; 124 | appearance: none; 125 | 126 | background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'), linear-gradient(to bottom, var(--color-ddark) 0%, var(--color-ddark) 100%); 127 | background-repeat: no-repeat, repeat; 128 | background-position: right .7em top 50%, 0 0; 129 | background-size: .65em auto, 100%; 130 | padding-right: 26pt; 131 | 132 | color: var(--color-dlight); 133 | font-size: 10pt; 134 | text-align: center; 135 | border: 0; 136 | } 137 | 138 | .outer-panel-header { 139 | background: var(--color-ddark); 140 | color: var(--color-dlight); 141 | font-size: 7pt; 142 | text-align: center; 143 | padding: 0px; 144 | /* text-align: center; */ 145 | } 146 | 147 | .panel { 148 | background: var(--color-dark); 149 | color: var(--color-light); 150 | text-align: left; 151 | font-size: 6pt; 152 | padding: 5pt; 153 | } 154 | 155 | 156 | .container { 157 | display: grid; 158 | /* grid-template-columns: repeat(2, 1fr); */ 159 | grid-template-columns: repeat(auto-fit, minmax(350px, 450px)); 160 | /* grid-template-rows: repeat(2, 1fr); */ 161 | /* grid-auto-rows: minmax(0,auto); */ 162 | /* background: var(--color-dark); */ 163 | grid-gap: 0.4em; 164 | } 165 | 166 | /* 167 | .mainboard { 168 | } 169 | */ 170 | 171 | .maintext { 172 | font-size: 8pt; 173 | padding-left: 1em; 174 | padding-right: 1em; 175 | } 176 | 177 | .submain { 178 | border: var(--color-xdddark) 1px solid; 179 | display: grid; 180 | grid-template-columns: 260px 1fr; 181 | grid-template-rows: 20pt 1fr; 182 | } 183 | 184 | .submain2 { 185 | display: grid; 186 | grid-template-columns: 260px 1fr; 187 | grid-template-rows: 10pt 1fr; 188 | } 189 | 190 | .controls { 191 | background: var(--color-ddark); 192 | border: var(--color-xdddark) 1px solid; 193 | } 194 | 195 | .miniboard1 { 196 | border: var(--color-xdddark) 1px solid; 197 | /* 198 | grid-column: 2/3; 199 | grid-row: 1/2; 200 | */ 201 | /* background: #888; */ 202 | } 203 | 204 | .submini { 205 | display: grid; 206 | grid-template-columns: 120px 1fr; 207 | grid-template-rows: 10pt 1fr; 208 | } 209 | 210 | .miniboard2 { 211 | border: var(--color-xdddark) 1px solid; 212 | /* 213 | grid-column: 2/3; 214 | grid-row: 2/3; 215 | */ 216 | /* background: #777; */ 217 | } 218 | 219 | div.board { 220 | float: left; 221 | } 222 | 223 | .variant { 224 | font-size: 6pt; 225 | padding-bottom: 0.25em; 226 | padding-left: 4em; 227 | text-indent: -4em; 228 | } 229 | 230 | .variant:nth-child(odd) { 231 | font-size: 6pt; 232 | padding-bottom: 0.25em; 233 | background: var(--color-ldark); 234 | } 235 | 236 | .movenr { 237 | font-size: 6pt; 238 | color: var(--color-turquoise); 239 | } 240 | 241 | .leadt { 242 | font-size: 6pt; 243 | color: var(--color-dlight); 244 | } 245 | 246 | .mainmove { 247 | font-size: 6pt; 248 | font-weight: bold; 249 | /* background: var(--color-xdddark); */ 250 | } 251 | 252 | .movenrb { 253 | font-size: 8pt; 254 | color: var(--color-turquoise); 255 | } 256 | 257 | .cm-chessboard.has-border .board .border-inner { 258 | fill: var(--color-ldark); 259 | } 260 | 261 | .cm-chessboard.has-border .board .border { 262 | fill: var(--color-light); 263 | } 264 | 265 | .cm-chessboard .board .square.white { 266 | fill: var(--color-dlight); 267 | } 268 | 269 | .cm-chessboard .board .square.black { 270 | fill: var(--color-turquoise); 271 | } 272 | 273 | .button { 274 | background: var(--color-dlight); 275 | color: var(--color-ddark); 276 | border: none; 277 | padding: 4px; 278 | text-align: center; 279 | text-decoration: none; 280 | display: inline-block; 281 | font-size: 10px; 282 | margin: 4px 2px; 283 | border-radius: 2px; 284 | } 285 | 286 | .setupbutton { 287 | background: var(--color-dlight); 288 | color: var(--color-ddark); 289 | border: none; 290 | padding: 1px; 291 | /* vertical-align: top; */ 292 | text-align: center; 293 | text-decoration: none; 294 | display: inline-block; 295 | font-size: 9px; 296 | margin: 4px 2px 2px 2px; 297 | border-radius: 2px; 298 | } 299 | 300 | .sbutton { 301 | background: var(--color-dlight); 302 | color: var(--color-ddark); 303 | border: none; 304 | padding: 2px; 305 | vertical-align: top; 306 | text-align: center; 307 | text-decoration: none; 308 | display: inline-block; 309 | font-size: 7px; 310 | margin: 4px 2px 2px 2px; 311 | border-radius: 2px; 312 | } 313 | 314 | .sbuttona { 315 | background: var(--color-light); 316 | color: var(--color-ddark); 317 | border: none; 318 | padding: 2px; 319 | vertical-align: top; 320 | text-align: center; 321 | text-decoration: none; 322 | display: inline-block; 323 | font-size: 7px; 324 | margin: 4px 2px 2px 2px; 325 | border-radius: 2px; 326 | } 327 | 328 | 329 | .label { 330 | font-size: 10px; 331 | color: var(--color-turquoise); 332 | margin: 4px 2px 2px 4px; 333 | } 334 | 335 | .input { 336 | background: var(--color-dlight); 337 | color: var(--color-xdddark); 338 | border: none; 339 | padding: 2.5px; 340 | text-align: left; 341 | text-decoration: none; 342 | display: inline-block; 343 | font-size: 10px; 344 | margin: 4px 2px; 345 | border-radius: 2px; 346 | vertical-align: -7pt; 347 | } 348 | 349 | .statscontainer { 350 | display: grid; 351 | /* grid-template-columns: repeat(2, 1fr); */ 352 | grid-template-columns: repeat(auto-fit, minmax(150px, 220px)); 353 | /* grid-template-rows: repeat(2, 1fr); */ 354 | /* grid-auto-rows: minmax(0,auto); */ 355 | /* background: var(--color-dark); */ 356 | grid-gap: 0.4em; 357 | } 358 | 359 | /* menu stuff */ 360 | 361 | .dropbtn { 362 | background-color: var(--color-ddark); 363 | color: var(--color-light); 364 | padding: 4px; 365 | font-size: 12px; 366 | border: none; 367 | cursor: pointer; 368 | border-radius: 0px; 369 | } 370 | 371 | .dropbtn:hover, 372 | .dropbtn:focus { 373 | background-color: var(--color-ldark); 374 | } 375 | 376 | .dropdown { 377 | position: relative; 378 | display: inline-block; 379 | } 380 | 381 | .dropdown-content { 382 | display: none; 383 | position: absolute; 384 | color: var(--color-light); 385 | background-color: var(--color-ddark); 386 | min-width: 100px; 387 | font-size: 12px; 388 | overflow: auto; 389 | box-shadow: 0px 8px 16px 0px rgba(10, 10, 10, 0.4); 390 | z-index: 1; 391 | } 392 | 393 | .dropdown-content-button { 394 | background-color: var(--color-ddark); 395 | color: var(--color-light); 396 | padding: 4px; 397 | font-size: 12px; 398 | border: none; 399 | cursor: pointer; 400 | display: block; 401 | border-radius: 0px; 402 | } 403 | 404 | .dropdown button:hover { 405 | background-color: var(--color-turquoise); 406 | } 407 | 408 | .show { 409 | display: block; 410 | } -------------------------------------------------------------------------------- /resources/chess_svg/Chess_bdt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_blt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_kdt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 12 | 15 | 18 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_klt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 14 | 17 | 20 | 23 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_ndt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 14 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_nlt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_pdt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_plt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_qdt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_qlt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_rdt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /resources/chess_svg/Chess_rlt45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 11 | 14 | 16 | 19 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/chess_svg/create_pngs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # Uses ImageMagick convert 3 | 4 | DEST_SIZE=60 5 | DENSITY=128 6 | 7 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_bdt45.svg ../../mchess/resources/pieces/bb"$DEST_SIZE".png 8 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_blt45.svg ../../mchess/resources/pieces/wb"$DEST_SIZE".png 9 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_kdt45.svg ../../mchess/resources/pieces/bk"$DEST_SIZE".png 10 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_klt45.svg ../../mchess/resources/pieces/wk"$DEST_SIZE".png 11 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_ndt45.svg ../../mchess/resources/pieces/bn"$DEST_SIZE".png 12 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_nlt45.svg ../../mchess/resources/pieces/wn"$DEST_SIZE".png 13 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_pdt45.svg ../../mchess/resources/pieces/bp"$DEST_SIZE".png 14 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_plt45.svg ../../mchess/resources/pieces/wp"$DEST_SIZE".png 15 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_qdt45.svg ../../mchess/resources/pieces/bq"$DEST_SIZE".png 16 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_qlt45.svg ../../mchess/resources/pieces/wq"$DEST_SIZE".png 17 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_rdt45.svg ../../mchess/resources/pieces/br"$DEST_SIZE".png 18 | convert -background transparent -density "$DENSITY" -resize "$DEST_SIZE"x Chess_rlt45.svg ../../mchess/resources/pieces/wr"$DEST_SIZE".png 19 | cp license.md ../../mchess/resources/pieces/ 20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/chess_svg/license.md: -------------------------------------------------------------------------------- 1 | From: https://commons.wikimedia.org/wiki/Template:SVG_chess_pieces 2 | by [Cburnett](https://en.wikipedia.org/wiki/User:Cburnett) 3 | 4 | Licensed as: GFDL & BSD & GPL 5 | 6 | BSD: 7 | 8 | Copyright © The author 9 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 10 | 11 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 12 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 13 | Neither the name of The author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /resources/turquoise.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domschl/python-mchess/74ccfd406f4fb99e85891a42f27213d7d796f671/resources/turquoise.blend --------------------------------------------------------------------------------