chess-mnemonic-protocol 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
chess_engine.py ADDED
@@ -0,0 +1,423 @@
1
+ FILES = 'abcdefgh'
2
+ PROMOTIONS = 'qrbn'
3
+ KNIGHT_STEPS = ((1,2),(2,1),(2,-1),(1,-2),(-1,-2),(-2,-1),(-2,1),(-1,2))
4
+ BISHOP_DIRS = ((1,1),(1,-1),(-1,1),(-1,-1))
5
+ ROOK_DIRS = ((1,0),(-1,0),(0,1),(0,-1))
6
+
7
+
8
+ def _sq(file, rank):
9
+ return rank * 8 + file
10
+
11
+
12
+ def _file(square):
13
+ return square & 7
14
+
15
+
16
+ def _rank(square):
17
+ return square >> 3
18
+
19
+
20
+ def _inside(file, rank):
21
+ return 0 <= file < 8 and 0 <= rank < 8
22
+
23
+
24
+ def _other(color):
25
+ return 'b' if color == 'w' else 'w'
26
+
27
+
28
+ def _color(piece):
29
+ if not piece:
30
+ return None
31
+ return 'w' if piece.isupper() else 'b'
32
+
33
+
34
+ def _type(piece):
35
+ if not piece:
36
+ return None
37
+ return piece.lower()
38
+
39
+
40
+ def _name(square):
41
+ return '%s%d' % (FILES[_file(square)], _rank(square) + 1)
42
+
43
+
44
+ def initial_state():
45
+ board = [None] * 64
46
+ back = 'rnbqkbnr'
47
+
48
+ for file in range(8):
49
+ board[_sq(file, 0)] = back[file].upper()
50
+ board[_sq(file, 1)] = 'P'
51
+ board[_sq(file, 6)] = 'p'
52
+ board[_sq(file, 7)] = back[file]
53
+
54
+ return {
55
+ 'board': board,
56
+ 'turn': 'w',
57
+ 'castling': set('KQkq'),
58
+ 'ep': None,
59
+ }
60
+
61
+
62
+ def _copy(state):
63
+ return {
64
+ 'board': state['board'].copy(),
65
+ 'turn': state['turn'],
66
+ 'castling': state['castling'].copy(),
67
+ 'ep': state['ep'],
68
+ }
69
+
70
+
71
+ def _attacked(state, target, color):
72
+ board = state['board']
73
+ target_file = _file(target)
74
+ target_rank = _rank(target)
75
+ pawn_rank = target_rank + (-1 if color == 'w' else 1)
76
+
77
+ for df in (-1, 1):
78
+ file = target_file + df
79
+ if _inside(file, pawn_rank):
80
+ piece = board[_sq(file, pawn_rank)]
81
+ if piece and _color(piece) == color and _type(piece) == 'p':
82
+ return True
83
+
84
+ for df, dr in KNIGHT_STEPS:
85
+ file = target_file + df
86
+ rank = target_rank + dr
87
+ if _inside(file, rank):
88
+ piece = board[_sq(file, rank)]
89
+ if piece and _color(piece) == color and _type(piece) == 'n':
90
+ return True
91
+
92
+ for dirs, pieces in ((BISHOP_DIRS, 'bq'), (ROOK_DIRS, 'rq')):
93
+ for df, dr in dirs:
94
+ file = target_file + df
95
+ rank = target_rank + dr
96
+ while _inside(file, rank):
97
+ piece = board[_sq(file, rank)]
98
+ if piece:
99
+ if _color(piece) == color and _type(piece) in pieces:
100
+ return True
101
+ break
102
+ file += df
103
+ rank += dr
104
+
105
+ for df in (-1, 0, 1):
106
+ for dr in (-1, 0, 1):
107
+ if not (df or dr):
108
+ continue
109
+ file = target_file + df
110
+ rank = target_rank + dr
111
+ if _inside(file, rank):
112
+ piece = board[_sq(file, rank)]
113
+ if piece and _color(piece) == color and _type(piece) == 'k':
114
+ return True
115
+
116
+ return False
117
+
118
+
119
+ def _in_check(state, color):
120
+ king = 'K' if color == 'w' else 'k'
121
+ try:
122
+ square = state['board'].index(king)
123
+ except ValueError:
124
+ return True
125
+ return _attacked(state, square, _other(color))
126
+
127
+
128
+ def _castling_moves(state, start, add):
129
+ board = state['board']
130
+ turn = state['turn']
131
+ rank = 0 if turn == 'w' else 7
132
+
133
+ if start != _sq(4, rank) or _in_check(state, turn):
134
+ return
135
+
136
+ enemy = _other(turn)
137
+ rook = 'R' if turn == 'w' else 'r'
138
+ king_side = 'K' if turn == 'w' else 'k'
139
+ queen_side = 'Q' if turn == 'w' else 'q'
140
+
141
+ if king_side in state['castling'] and board[_sq(7, rank)] == rook:
142
+ if not board[_sq(5, rank)] and not board[_sq(6, rank)]:
143
+ safe = not _attacked(state, _sq(5, rank), enemy)
144
+ safe = safe and not _attacked(state, _sq(6, rank), enemy)
145
+ if safe:
146
+ add(start, _sq(6, rank))
147
+
148
+ if queen_side in state['castling'] and board[_sq(0, rank)] == rook:
149
+ clear = not board[_sq(1, rank)]
150
+ clear = clear and not board[_sq(2, rank)]
151
+ clear = clear and not board[_sq(3, rank)]
152
+ if clear:
153
+ safe = not _attacked(state, _sq(3, rank), enemy)
154
+ safe = safe and not _attacked(state, _sq(2, rank), enemy)
155
+ if safe:
156
+ add(start, _sq(2, rank))
157
+
158
+
159
+ def _pseudo_moves(state):
160
+ moves = []
161
+ board = state['board']
162
+ turn = state['turn']
163
+
164
+ def add(start, end, promotion=''):
165
+ moves.append((start, end, promotion))
166
+
167
+ for start, piece in enumerate(board):
168
+ if not piece or _color(piece) != turn:
169
+ continue
170
+
171
+ kind = _type(piece)
172
+ file = _file(start)
173
+ rank = _rank(start)
174
+
175
+ if kind == 'p':
176
+ step = 1 if turn == 'w' else -1
177
+ home = 1 if turn == 'w' else 6
178
+ promo = 7 if turn == 'w' else 0
179
+ one = rank + step
180
+
181
+ if _inside(file, one) and not board[_sq(file, one)]:
182
+ end = _sq(file, one)
183
+ if one == promo:
184
+ for promotion in PROMOTIONS:
185
+ add(start, end, promotion)
186
+ else:
187
+ add(start, end)
188
+
189
+ two = rank + (2 * step)
190
+ if rank == home and not board[_sq(file, two)]:
191
+ add(start, _sq(file, two))
192
+
193
+ for df in (-1, 1):
194
+ capture_file = file + df
195
+ capture_rank = rank + step
196
+ if not _inside(capture_file, capture_rank):
197
+ continue
198
+
199
+ end = _sq(capture_file, capture_rank)
200
+ target = board[end]
201
+ capture = target and _color(target) != turn
202
+ if capture or state['ep'] == end:
203
+ if capture_rank == promo:
204
+ for promotion in PROMOTIONS:
205
+ add(start, end, promotion)
206
+ else:
207
+ add(start, end)
208
+ continue
209
+
210
+ if kind == 'n':
211
+ for df, dr in KNIGHT_STEPS:
212
+ next_file = file + df
213
+ next_rank = rank + dr
214
+ if not _inside(next_file, next_rank):
215
+ continue
216
+ end = _sq(next_file, next_rank)
217
+ if not board[end] or _color(board[end]) != turn:
218
+ add(start, end)
219
+ continue
220
+
221
+ if kind in 'brq':
222
+ if kind == 'b':
223
+ dirs = BISHOP_DIRS
224
+ elif kind == 'r':
225
+ dirs = ROOK_DIRS
226
+ else:
227
+ dirs = BISHOP_DIRS + ROOK_DIRS
228
+
229
+ for df, dr in dirs:
230
+ next_file = file + df
231
+ next_rank = rank + dr
232
+ while _inside(next_file, next_rank):
233
+ end = _sq(next_file, next_rank)
234
+ if not board[end]:
235
+ add(start, end)
236
+ else:
237
+ if _color(board[end]) != turn:
238
+ add(start, end)
239
+ break
240
+ next_file += df
241
+ next_rank += dr
242
+ continue
243
+
244
+ if kind == 'k':
245
+ for df in (-1, 0, 1):
246
+ for dr in (-1, 0, 1):
247
+ if not (df or dr):
248
+ continue
249
+ next_file = file + df
250
+ next_rank = rank + dr
251
+ if _inside(next_file, next_rank):
252
+ end = _sq(next_file, next_rank)
253
+ if not board[end] or _color(board[end]) != turn:
254
+ add(start, end)
255
+ _castling_moves(state, start, add)
256
+
257
+ return moves
258
+
259
+
260
+ def _apply(state, move):
261
+ start, end, promotion = move
262
+ new_state = _copy(state)
263
+ board = new_state['board']
264
+ piece = board[start]
265
+ color = _color(piece)
266
+ kind = _type(piece)
267
+ captured = board[end]
268
+ start_name = _name(start)
269
+ end_name = _name(end)
270
+
271
+ board[start] = None
272
+
273
+ if kind == 'p' and state['ep'] == end and not captured:
274
+ if _file(start) != _file(end):
275
+ offset = -8 if color == 'w' else 8
276
+ board[end + offset] = None
277
+
278
+ if promotion:
279
+ piece = promotion.upper() if color == 'w' else promotion
280
+
281
+ board[end] = piece
282
+
283
+ if kind == 'k' and abs(_file(end) - _file(start)) == 2:
284
+ rank = _rank(start)
285
+ king_side = _file(end) == 6
286
+ rook_from = _sq(7 if king_side else 0, rank)
287
+ rook_to = _sq(5 if king_side else 3, rank)
288
+ board[rook_to] = board[rook_from]
289
+ board[rook_from] = None
290
+
291
+ if kind == 'k':
292
+ new_state['castling'].discard('K' if color == 'w' else 'k')
293
+ new_state['castling'].discard('Q' if color == 'w' else 'q')
294
+
295
+ rights = (('h1','K'),('a1','Q'),('h8','k'),('a8','q'))
296
+ for square, right in rights:
297
+ if start_name == square or end_name == square:
298
+ new_state['castling'].discard(right)
299
+
300
+ new_state['ep'] = None
301
+ if kind == 'p' and abs(_rank(end) - _rank(start)) == 2:
302
+ new_state['ep'] = (start + end) >> 1
303
+
304
+ new_state['turn'] = _other(state['turn'])
305
+ return new_state
306
+
307
+
308
+ def _uci(move):
309
+ start, end, promotion = move
310
+ return _name(start) + _name(end) + promotion
311
+
312
+
313
+ def _legal_objects(state):
314
+ color = state['turn']
315
+ moves = []
316
+
317
+ for move in _pseudo_moves(state):
318
+ if not _in_check(_apply(state, move), color):
319
+ moves.append(move)
320
+
321
+ return moves
322
+
323
+
324
+ def _find_move(state, uci):
325
+ uci = str(uci).strip().lower()
326
+ for move in _legal_objects(state):
327
+ if _uci(move) == uci:
328
+ return move
329
+ raise ValueError('illegal move: %s' % uci)
330
+
331
+
332
+ def legal_moves(state):
333
+ moves = [_uci(move) for move in _legal_objects(state)]
334
+ moves.sort()
335
+ return moves
336
+
337
+
338
+ def _is_capture(state, move):
339
+ start, end, unused = move
340
+ piece = state['board'][start]
341
+
342
+ if state['board'][end]:
343
+ return True
344
+
345
+ return (
346
+ _type(piece) == 'p'
347
+ and state['ep'] == end
348
+ and _file(start) != _file(end)
349
+ )
350
+
351
+
352
+ def _san_prefix(state, move, legal):
353
+ start, end, unused = move
354
+ board = state['board']
355
+ kind = _type(board[start])
356
+
357
+ if kind == 'p':
358
+ return FILES[_file(start)] if _is_capture(state, move) else ''
359
+
360
+ prefix = kind.upper()
361
+ others = []
362
+ for candidate in legal:
363
+ if candidate == move or candidate[1] != end:
364
+ continue
365
+ if _type(board[candidate[0]]) == kind:
366
+ others.append(candidate)
367
+
368
+ if not others:
369
+ return prefix
370
+
371
+ same_file = any(_file(other[0]) == _file(start) for other in others)
372
+ same_rank = any(_rank(other[0]) == _rank(start) for other in others)
373
+
374
+ if not same_file:
375
+ return prefix + FILES[_file(start)]
376
+ if not same_rank:
377
+ return prefix + str(_rank(start) + 1)
378
+ return prefix + _name(start)
379
+
380
+
381
+ def san(state, uci):
382
+ legal = _legal_objects(state)
383
+ uci = str(uci).strip().lower()
384
+ move = None
385
+
386
+ for candidate in legal:
387
+ if _uci(candidate) == uci:
388
+ move = candidate
389
+ break
390
+
391
+ if move is None:
392
+ raise ValueError('illegal move: %s' % uci)
393
+
394
+ start, end, promotion = move
395
+ kind = _type(state['board'][start])
396
+
397
+ if kind == 'k' and abs(_file(end) - _file(start)) == 2:
398
+ text = 'O-O' if _file(end) == 6 else 'O-O-O'
399
+ else:
400
+ text = _san_prefix(state, move, legal)
401
+ if _is_capture(state, move):
402
+ text += 'x'
403
+ text += _name(end)
404
+ if promotion:
405
+ text += '=' + promotion.upper()
406
+
407
+ next_state = _apply(state, move)
408
+ if _in_check(next_state, next_state['turn']):
409
+ text += '#' if not _legal_objects(next_state) else '+'
410
+
411
+ return text
412
+
413
+
414
+ def play(state, uci):
415
+ return _apply(state, _find_move(state, uci))
416
+
417
+
418
+ def result(state):
419
+ if _legal_objects(state):
420
+ return None
421
+ if not _in_check(state, state['turn']):
422
+ return '1/2-1/2'
423
+ return '0-1' if state['turn'] == 'w' else '1-0'
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: chess-mnemonic-protocol
3
+ Version: 0.1.0
4
+ Summary: Chess Mnemonic Protocol tools
5
+ Project-URL: Repository, https://github.com/julesora/cmp
6
+ Project-URL: Specification, https://github.com/julesora/cmp/blob/main/spec/cmp-0001.md
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+
12
+ CMP
13
+ ===
14
+
15
+ CMP is a small text format for legal chess move sequences.
16
+ CMP 1 stores moves in lowercase UCI.
17
+
18
+ Quick Start
19
+ -----------
20
+
21
+ * Check a value: cmp check cmp1 e2e4 e7e5
22
+ * Normalize a value: cmp normalize CMP1 E2E4 E7E5
23
+ * Convert to SAN: cmp convert --to san cmp1 e2e4 e7e5
24
+ * Convert to PGN: cmp convert --to pgn cmp1 e2e4 e7e5
25
+ * List legal moves: cmp legal e2e4 e7e5
26
+
27
+ Install for development:
28
+
29
+ python -m pip install -e .
30
+
31
+ Format
32
+ ------
33
+
34
+ A CMP 1 value starts with cmp1 followed by legal UCI moves:
35
+
36
+ cmp1 e2e4 e7e5 g1f3 b8c6
37
+
38
+ The main rules are:
39
+
40
+ * moves are lowercase UCI
41
+ * moves must be legal
42
+ * legal move lists are sorted
43
+ * a finished game resets to the start position
44
+
45
+ See spec/cmp-0001.md for the full format.
46
+
47
+ Python API
48
+ ----------
49
+
50
+ import cmp
51
+
52
+ line = 'cmp1 e2e4 e7e5 g1f3'
53
+
54
+ cmp.check(line)
55
+ cmp.normalize(line)
56
+ cmp.parse_mnemonic(line)
57
+ cmp.legal_moves()
58
+ cmp.convert_mnemonic(line, 'san')
59
+ cmp.convert_mnemonic(line, 'pgn')
60
+
61
+ CMP text always uses UCI. SAN and PGN are output formats.
62
+
63
+ Documentation
64
+ -------------
65
+
66
+ * Format: spec/cmp-0001.md
67
+ * Test vectors: vectors.json
68
+ * Changes: CHANGELOG.md
69
+ * Contributing: CONTRIBUTING.md
70
+ * Security: SECURITY.md
71
+
72
+ Source
73
+ ------
74
+
75
+ * cmp.py: parser, API and command line
76
+ * chess_engine.py: chess rules
77
+ * tests/test_cmp.py: tests
78
+
79
+ Development
80
+ -----------
81
+
82
+ Run the tests with:
83
+
84
+ python -m unittest discover -s tests -v
@@ -0,0 +1,7 @@
1
+ chess_engine.py,sha256=eQhsmuUfhBy4oJ-dxWPQFMtfwHII02PayL7fPhknCEc,11815
2
+ cmp.py,sha256=1YAS6EKZypasJk2RbuHhxkfBsWRcjAoyAVkYMoVXxiM,5598
3
+ chess_mnemonic_protocol-0.1.0.dist-info/METADATA,sha256=QVS0arBTMDTlyXxVgKieEw4crreK5mlFH2d1RK-iD_E,1817
4
+ chess_mnemonic_protocol-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ chess_mnemonic_protocol-0.1.0.dist-info/entry_points.txt,sha256=tIkb8_BjbE1Yygz4EwmcB3mdGhQAs9LTxXlFI0G8vUc,33
6
+ chess_mnemonic_protocol-0.1.0.dist-info/top_level.txt,sha256=zpYYTReL9I8m0dtm_mB4ZHGda7RZGrNv6mmawPpd2EU,17
7
+ chess_mnemonic_protocol-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cmp = cmp:main
@@ -0,0 +1,2 @@
1
+ chess_engine
2
+ cmp
cmp.py ADDED
@@ -0,0 +1,243 @@
1
+ import argparse
2
+ import re
3
+ import sys
4
+
5
+ from chess_engine import initial_state
6
+ from chess_engine import legal_moves as chess_moves
7
+ from chess_engine import play
8
+ from chess_engine import result as chess_result
9
+ from chess_engine import san as chess_san
10
+
11
+ VERSION = 'cmp1'
12
+ PROTOCOL_VERSION = VERSION
13
+ __version__ = '0.1.0'
14
+
15
+ UCI_RE = re.compile(r'^[a-h][1-8][a-h][1-8][qrbn]?$')
16
+
17
+
18
+ class CMPError(ValueError):
19
+ pass
20
+
21
+
22
+ class InvalidMnemonic(CMPError):
23
+ pass
24
+
25
+
26
+ def _split(mnemonic):
27
+ words = str(mnemonic).strip().lower().split()
28
+ if not words or words[0] != VERSION:
29
+ raise InvalidMnemonic('mnemonic must start with %s' % VERSION)
30
+ if len(words) == 1:
31
+ raise InvalidMnemonic('mnemonic has no moves')
32
+ return words[1:]
33
+
34
+
35
+ def _check_move(move, number):
36
+ if not UCI_RE.match(move):
37
+ raise InvalidMnemonic(
38
+ 'move %d is not canonical UCI: %s' % (number, move)
39
+ )
40
+
41
+
42
+ def _next(state, move, number):
43
+ _check_move(move, number)
44
+
45
+ if not chess_moves(state):
46
+ state = initial_state()
47
+
48
+ try:
49
+ return play(state, move)
50
+ except ValueError:
51
+ raise InvalidMnemonic('illegal move %d (%s)' % (number, move))
52
+
53
+
54
+ def _play_moves(moves):
55
+ state = initial_state()
56
+ for number, move in enumerate(moves, 1):
57
+ state = _next(state, move, number)
58
+ return state
59
+
60
+
61
+ def parse_mnemonic(mnemonic):
62
+ moves = _split(mnemonic)
63
+ _play_moves(moves)
64
+ return tuple(moves)
65
+
66
+
67
+ def normalize(mnemonic):
68
+ return VERSION + ' ' + ' '.join(parse_mnemonic(mnemonic))
69
+
70
+
71
+ def check(mnemonic):
72
+ try:
73
+ parse_mnemonic(mnemonic)
74
+ return True
75
+ except (TypeError, ValueError):
76
+ return False
77
+
78
+
79
+ def _moves(value):
80
+ if isinstance(value, str):
81
+ words = value.strip().lower().split()
82
+ if words and words[0] == VERSION:
83
+ return list(parse_mnemonic(value))
84
+ return words
85
+ return [str(move).strip().lower() for move in value]
86
+
87
+
88
+ def legal_moves(moves=()):
89
+ moves = _moves(moves)
90
+ state = initial_state()
91
+
92
+ for number, move in enumerate(moves, 1):
93
+ state = _next(state, move, number)
94
+
95
+ return chess_moves(state)
96
+
97
+
98
+ def _san_games(moves):
99
+ games = []
100
+ game = []
101
+ state = initial_state()
102
+
103
+ for move in moves:
104
+ if not chess_moves(state):
105
+ games.append((game, chess_result(state)))
106
+ game = []
107
+ state = initial_state()
108
+
109
+ game.append(chess_san(state, move))
110
+ state = play(state, move)
111
+
112
+ games.append((game, chess_result(state)))
113
+ return games
114
+
115
+
116
+ def _san_text(moves):
117
+ words = []
118
+
119
+ for number, move in enumerate(moves):
120
+ if number % 2 == 0:
121
+ words.append('%d. %s' % ((number // 2) + 1, move))
122
+ else:
123
+ words.append(move)
124
+
125
+ return ' '.join(words)
126
+
127
+
128
+ def _pgn(game, result, number):
129
+ result = result or '*'
130
+ headers = [
131
+ '[Event "CMP-1"]',
132
+ '[Site "?"]',
133
+ '[Date "????.??.??"]',
134
+ '[Round "%d"]' % number,
135
+ '[White "?"]',
136
+ '[Black "?"]',
137
+ '[Result "%s"]' % result,
138
+ ]
139
+
140
+ moves = _san_text(game)
141
+ if moves:
142
+ moves += ' '
143
+ moves += result
144
+
145
+ return '\n'.join(headers) + '\n\n' + moves
146
+
147
+
148
+ def convert_mnemonic(mnemonic, format='pgn'):
149
+ moves = parse_mnemonic(mnemonic)
150
+ format = str(format).strip().lower()
151
+
152
+ if format == 'uci':
153
+ return ' '.join(moves)
154
+
155
+ games = _san_games(moves)
156
+
157
+ if format == 'san':
158
+ return '\n'.join(_san_text(game) for game, result in games)
159
+
160
+ if format == 'pgn':
161
+ blocks = []
162
+ for number, item in enumerate(games, 1):
163
+ game, result = item
164
+ blocks.append(_pgn(game, result, number))
165
+ return '\n\n'.join(blocks)
166
+
167
+ raise ValueError('format must be uci, san, or pgn')
168
+
169
+
170
+ def _parser():
171
+ parser = argparse.ArgumentParser(prog='cmp')
172
+ parser.add_argument(
173
+ '--version',
174
+ action='version',
175
+ version='cmp %s (protocol %s)' % (__version__, VERSION),
176
+ )
177
+
178
+ commands = parser.add_subparsers(dest='command', required=True)
179
+
180
+ command = commands.add_parser('check', help='validate a mnemonic')
181
+ command.add_argument('mnemonic', nargs='+')
182
+
183
+ command = commands.add_parser('normalize', help='normalize a mnemonic')
184
+ command.add_argument('mnemonic', nargs='+')
185
+
186
+ command = commands.add_parser('convert', help='convert chess notation')
187
+ command.add_argument('--to', choices=('uci', 'san', 'pgn'), default='pgn')
188
+ command.add_argument('mnemonic', nargs='+')
189
+
190
+ command = commands.add_parser('legal', help='list legal moves')
191
+ command.add_argument('moves', nargs='*')
192
+
193
+ return parser
194
+
195
+
196
+ def _text(words):
197
+ return ' '.join(words)
198
+
199
+
200
+ def main(argv=None):
201
+ args = _parser().parse_args(argv)
202
+
203
+ if args.command == 'check':
204
+ try:
205
+ parse_mnemonic(_text(args.mnemonic))
206
+ except InvalidMnemonic as error:
207
+ print('invalid: %s' % error, file=sys.stderr)
208
+ return 1
209
+ print('valid')
210
+ return 0
211
+
212
+ if args.command == 'normalize':
213
+ print(normalize(_text(args.mnemonic)))
214
+ return 0
215
+
216
+ if args.command == 'convert':
217
+ print(convert_mnemonic(_text(args.mnemonic), args.to))
218
+ return 0
219
+
220
+ if args.command == 'legal':
221
+ print(' '.join(legal_moves(args.moves)))
222
+ return 0
223
+
224
+ return 2
225
+
226
+
227
+ __all__ = [
228
+ 'CMPError',
229
+ 'InvalidMnemonic',
230
+ 'PROTOCOL_VERSION',
231
+ 'VERSION',
232
+ '__version__',
233
+ 'check',
234
+ 'convert_mnemonic',
235
+ 'legal_moves',
236
+ 'main',
237
+ 'normalize',
238
+ 'parse_mnemonic',
239
+ ]
240
+
241
+
242
+ if __name__ == '__main__':
243
+ raise SystemExit(main())