%load_ext autoreload
%autoreload 2

1. Motivation

For automated insights, we need a way to let the computer learn about our metrics. We need something to compare against our metrics, so I figured I start with stockfish, a very popular chess engine. Python chess also interfaces with stockfish so I won't need another dependency, either.

Stockfish is a small download and with the binaries having few dependencies they should be ready to go right after extracting the archive – no extra installation required. The engine should then help us by generating our training data.

2. Getting the engine

!mkdir -p "tools"
!ls
00_core.ipynb	    cheviz.egg-info	images		README.md
01_data.ipynb	    CONTRIBUTING.md	index.ipynb	requirements.txt
02_ui.ipynb	    data		jupyterlab.log	settings.ini
03_stockfish.ipynb  docs		LICENSE		setup.py
04_processor.ipynb  env.source		Makefile	tools
cheviz		    env.source_EXAMPLE	MANIFEST.in

Assuming this notebook runs on a Linux box, we'll get the Linux binary from the official website. We're not building high-end desktop software here (does such thing even exist?) so hard-coding the download URL for just the Linux version is OK. It might feel wrong, but the best advice to counter your (very valid) intuition is that we have to focus on our goal: automated insights. That is, don't spend time over-engineering the basic, non-data-sciency stuff in your pipeline unless your pipeline is ready to run in production and earn money for you. There is a reason why we use Python for all of this, so quick'n'dirty – to a certain degree – is the way to go.

We need to set a fake browser user-agent for our download request, otherwise we get 403'd. Normally that's a sign you're doing something wrong at extra costs to someone else (in this case ISP hosting fees for the stockfish communiy). At 1.7M of download size, which is probably smaller than a typical project's landing page these days, I don't feel guilty at all.

download[source]

download()

We still need to extract the downloaded Zip archive. Also, we need to make the stockfish binary executable. There are 3 binaries available in this version, we use what I guess is the default one. More quick'n'dirty hardcoding that I expect to break sooner than later.

extract[source]

extract(dst:Path)

stockfish = extract(download())

Check if we can run stockfish now, but let's not get stuck in stockfish's command prompt. So we send 'quit' to it immediately, which will still return the version of the binary and its authors.

!{stockfish} quit
Stockfish 11 64 by T. Romstad, M. Costalba, J. Kiiski, G. Linscott

The next step is to hook up the engine with Python chess, as described in their documentation: https://python-chess.readthedocs.io/en/latest/engine.html

PGN can be described as a hierarchical document model due to its ability to store variations next to the mainline. Python chess uses a recursive nodes structure to model PGN; GameNode::add_main_variation(move: chess.Move)->GameNode processes a move, adds it as a child to the current node and returns the child. If we only process the mainline moves, the structure effectively represents a linked list. We could also just use a chess.Board instance to replay the engine results back to Python chess. Since our data module handles PGN however, I thought I go the extra step here. The way engine, game and board interact feels fragile to me but that is perhaps because board isn't just a plain chess position viewer. Instead, it has game/engine logic of its own.

makeEngine[source]

makeEngine(engine_path:Path=None)

playGame[source]

playGame(engine_path:Path, time_limit:float=0.1)

Computer chess is extremely technical and moves will pile up. Even with short time limits a chess engine vs chess engine game can take a couple seconds or even minutes. Too short of a time limit and computer chess turns into garbage. Notice that it's not the engine that's slow, it's how we interact with it and process results.

%time game = playGame(stockfish)
display(game.mainline_moves())
CPU times: user 953 ms, sys: 27.2 ms, total: 980 ms
Wall time: 18 s
<Mainline at 0x7f8718232ac8 (1. e4 e5 2. Nf3 Nc6 3. Bc4 Nf6 4. d3 Bc5 5. O-O d6 6. Bg5 h6 7. Be3 Bb6 8. Qe1 O-O 9. a4 Qe8 10. b4 Bxe3 11. fxe3 a6 12. Qc3 Ne7 13. Nbd2 Ng6 14. a5 Qd7 15. Ra2 b5 16. axb6 cxb6 17. Qa1 b5 18. Bb3 Qa7 19. Re1 Qb6 20. h3 Kh7 21. Kh2 Qc7 22. Bd5 Rb8 23. Kh1 Ne7 24. Bb3 Kg8 25. Kg1 Rd8 26. Qb2 Bb7 27. c4 Rbc8 28. Raa1 Ng6 29. Rec1 Qe7 30. Bd1 bxc4 31. dxc4 Nxe4 32. Nxe4 Bxe4 33. Rxa6 Bd3 34. b5 Bxc4 35. Rc6 d5 36. Be2 Rxc6 37. bxc6 Qc5 38. Rc3 Qxc6 39. Nd2 Nh4 40. Bf1 Qg6 41. Nxc4 dxc4 42. Qf2 Nf5 43. Bxc4 Rd1+ 44. Kh2 e4 45. Bb3 Qd6+ 46. Qf4 Rd3 47. Rc8+ Kh7 48. Bxf7 g5 49. Qxd6 Nxd6 50. Rc7 Nxf7 51. Rxf7+ Kg6 52. Re7 Rxe3 53. g4 h5 54. gxh5+ Kxh5 55. Rg7 Rc3 56. Kg2 Rc2+ 57. Kg3 Rc5 58. Re7 Rc3+ 59. Kg2 Re3 60. Rg7 Rb3 61. Re7 Re3 62. Rg7 Rb3 63. Rg8 Rf3 64. Re8 Re3 65. Rg8 Rd3 66. Re8 Rd2+ 67. Kg3 Rd4 68. Kf2 Rd3 69. Kg2 Re3 70. Rg8 Ra3 71. Rh8+ Kg6 72. h4 gxh4 73. Rxh4 Ra2+ 74. Kg3 Ra4 75. Rh3 Kg5 76. Rh8 Ra2 77. Rd8 Kf5 78. Rd5+ Ke6 79. Rh5 e3 80. Kf4 e2 81. Re5+ Kd7 82. Kf3 Kd6 83. Re4 Rb2 84. Re3 Rd2 85. Re8 Kc6 86. Rxe2 Rxe2 87. Kxe2)>

Let's look at the engine's game through our UI.

import cheviz.ui
games_list = [cheviz.data.fromGame(game)]
cheviz.ui.showGameUi(games_list)

We use the same display wrap trick as in the 02_ui.ipynb notebook.

from IPython.display import display, HTML

CSS = """
.output {
    flex-direction: row;
    flex-wrap: wrap;
}
"""

HTML('<style>{}</style>'.format(CSS))