### Novag Citrine Interface with GUI

Here is the code:

import serial, time, chess, chess.uci
import tkinter as tk

class ChessBoard(chess.Board):
''' This class is chess.Board with the addition of a Standard Algebraic
Notation (SAN) stack, and some additional methods.
'''
def __init__(self, fen=
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
chess960=False):
''' Initialize chess.Board and the SAN move stack.
'''
chess.Board.__init__(self, fen, chess960)
self.san_stack = []

def pushem(self, uci):
''' Push a UCI formatted move onto chess.Board, and onto the SAN stack.
'''
move = chess.Board.parse_uci(self, uci)
san = chess.Board.san(self, move)
self.san_stack.append(san)
chess.Board.push(self, move)

def popem(self):
''' Pop the last move from chess.Board, and from the SAN stack.
'''
chess.Board.pop(self)
return self.san_stack.pop()

def pgn(self):
''' Return the PGN for the game.
'''
n, out = 1, []
for m in self.san_stack:
n += 1
out.append(((str(n//2) + '.') if n%2 == 0 else '') + str(m))
return ' '.join(out)

def unicode(self):
''' Return a Unicode version of the simple Ascii board.
'''
uni_sym = {' ': ' ', '\n': '\n', '.': u'\u00B7',
'R': u'♖', 'r': u'♜', 'N': u'♘', 'n': u'♞',
'B': u'♗', 'b': u'♝', 'Q': u'♕', 'q': u'♛',
'K': u'♔', 'k': u'♚', 'P': u'♙', 'p': u'♟'}
return ''.join([uni_sym[char] for char in str(self)])

def last_san_move(self):
''' Return a SAN representation of the last move played, or "New Game",
if it is a new game.
'''
_half_moves = len(self.san_stack)
if _half_moves == 0:
return 'New Game'
else:
_dots =  '.' if _half_moves % 2 == 1 else '...'
return str((_half_moves+1)//2) + _dots + self.san_stack[-1]

ENGINE_OPTIONS = ['PC Engine', 'Novag BE8']
for level in range(1,9):
ENGINE_OPTIONS.append('Novag AT' + str(level))

class GUI(tk.Tk):
''' Graphical User Interface (GUI).
'''
def __init__(self):
''' Create and initialize a new GUI object.
'''
tk.Tk.__init__(self)
self.title('Novag Citrine Interface')
top_frame = tk.Frame(self)
self.start_stop_button = tk.Button(top_frame, text='Start Engine',
bg='light green', command=self._start_stop_button_pressed)
self.start_stop_button.pack(side='left')
self.engine_option = tk.StringVar()
self.engine_option.set(ENGINE_OPTIONS[0])
self.engine_option.trace('w', self._engine_option_changed)
*ENGINE_OPTIONS)
self.position_label = tk.Label(self, text=board.unicode(),
self.position_label.pack()
self.move_label = tk.Label(self, text='New Game',
self.move_label.pack()
self.engine_on = self.engine_move_now = False

def _start_stop_button_pressed(self):
''' Respond to an Engine Start/Stop button press.
'''
if self.engine_on:
self.engine_on = False
self.start_stop_button['text'] = 'Start Engine'
self.start_stop_button['bg'] = 'light green'
else:
self.engine_on = self.engine_move_now = True
self.start_stop_button['text'] = 'Stop Engine'
self.start_stop_button['bg'] = 'pink'

def _engine_option_changed(self, *args):
''' Respond to a change in the engine option by changing the Citrine
playing level.
'''
option = self.engine_option.get()
if option[:6] == 'Novag ':
send_command('l ' + option[6:].lower())
else:
send_command('l tr8')

def update_posn(self):
''' Update the position and last move.
'''
self.position_label['text'] = board.unicode()
self.move_label['text'] = board.last_san_move()
self.update()

def new_game(self):
''' User has reset the pieces on the Novag to start a new game.
'''
self.engine_on = False
self.start_stop_button['text'] = 'Start Engine'
self.start_stop_button['bg'] = 'light green'
self.update_posn()

def parse_novag(novag_move):
''' Split a Novag move or take-back response into four fields: M or T,
move number, move colour, and the move converted to UCI format.
'''
m_or_t, mn_c, mv = novag_move.split()
mn = int(mn_c.strip(',')) # Move number.
mc = mn_c[-1] != ',' # True for a white move.
if mv == 'O-O':
if mc:
return (m_or_t, mn, mc, 'e1g1')
else:
return (m_or_t, mn, mc, 'e8g8')
elif mv == 'O-O-O':
if mc:
return (m_or_t, mn, mc, 'e1c1')
else:
return (m_or_t, mn, mc, 'e8c8')
elif len(mv) == 5 or mv[5:7] == 'ep':
return (m_or_t, mn, mc, mv[0:2] + mv[3:5])
else:
return (m_or_t, mn, mc, mv[0:2] + mv[3:5] + mv[6].lower())

def novag_move(uci_move, board):
''' Convert a UCI move to a lowercase Novag format move, in the context
of the board position.
'''
if uci_move == 'e1g1':
return 'O-O' if board.piece_at(chess.E1) == 'K' else uci_move
elif uci_move == 'e1c1':
return 'O-O-O' if board.piece_at(chess.E1) == 'K' else uci_move
elif uci_move == 'e8g8':
return 'O-O' if board.piece_at(chess.E8) == 'k' else uci_move
elif uci_move =='e8c8':
return 'O-O-O'if board.piece_at(chess.E8) == 'k' else uci_move
elif len(uci_move) == 5:
return uci_move[:4] + '/' + uci_move[4]
else:
return uci_move

def send_command(command):
''' Send a command from the PC to the Citrine.
'''
ser.write((command + '\r\n').encode(encoding='UTF-8'))
time.sleep(0.1)

def new_game():
''' Initialize a new game.
'''
global board
try:
print(board.pgn())
except:
pass
board = ChessBoard()
send_command('n') # New Game.
send_command('u on') # Turn Referee Mode on.
send_command('x on') # Turn Xmit on.
send_command('l tr8') # Set the Level.

while True:
if line == '':
break
print(line)

def engine_move():
''' Calculate a move using the onboard Citrine engine, or calculate a move
with the PC engine and send it to the Citrine.
'''
global engine_turn
if gui.engine_option.get()[:6] == 'Novag ':
print('Novag Engine Move')
send_command('j')
gui.update_posn()
engine_turn = False
elif board.is_game_over(): # PC engine move and game over.
engine_turn = True
else:
engine.position(board) # PC engine move and game ongoing
uci_move = engine.go(movetime=100).bestmove.uci()
nv_move = novag_move(uci_move, board)
print('PC Engine Move:', nv_move)
if ser.inWaiting() == 0: # Check that there are no take-backs.
send_command('m' + nv_move)
if board.is_capture(board.parse_uci(uci_move)):
time.sleep(3.0)
send_command('m' + nv_move)
engine_turn = False
else:
engine_turn = True

GAME_RESULT = ('Draw by repetion', 'Draw by 50 move rule',
'Draw by insufficient material', 'Stalemate',
'Checkmate', 'Citrine resigns')

def respond_to_novag():
''' Respond to replies from the Citrine, which should be moves,
optionally followed by take backs. If the reply begins with 'M' it
is a move, and if it begins with 'T' it is a take-back. 'New Game'
indicates that the user has reset the pieces to start a new game.
The final line of this function returns to the Tkinter main loop and
requests a call back after 100 mS.
'''
global engine_turn # False if the echoed move is the engine's move.
if line != '':
print(line)
if line[0] == 'M':
if line[1] == '#': # Game over echoed.
print(GAME_RESULT[int(line[2])-1])
else: # Move echoed.
mt, mn, mc, mv = parse_novag(line)
if mn != board.fullmove_number or mc != board.turn:
raise ValueError
board.pushem(mv)
gui.update_posn()
if gui.engine_on and engine_turn:
engine_move()
else:
engine_turn = True
elif line[0] == 'T': # Take-back echoed.
mt, mn, mc, mv = parse_novag(line)
board.popem()
if mn != board.fullmove_number or mc != board.turn:
raise ValueError
gui.update_posn()
elif line[:8] == 'New Game':
new_game() # User has reset the start position.
gui.new_game()
elif gui.engine_move_now:
gui.engine_move_now = False
engine_move()
gui.after(100, respond_to_novag)

def print_engine_options(eo):
''' Print engine options.
'''
print('Engine Option         Type    Default Min    Max     Var')
for k in sorted(eo):
show_empty = lambda x : x if x != '' else "''"
option = str(k).ljust(22) + str(eo[k].type).ljust(8) + \
show_empty(str(eo[k].default)).ljust(8) + \
str(eo[k].min).ljust(7) + str(eo[k].max).ljust(8) + \
' '.join([str(v) for v in eo[k].var])
print(option)

port = '/dev/ttyUSB0'
ser = serial.Serial(port, 57600, timeout=0.05)
print(ser.name, 'opened')
engine = chess.uci.popen_engine('/home/geoff/scid/stockfish 7 x64 bmi2')
engine.uci()
print(engine.name)
print_engine_options(engine.options)
engine.setoption({'Skill Level' : 7})
new_game()
gui = GUI()
respond_to_novag()
gui.mainloop()
if board.pgn() != '':
print(board.pgn())
engine.quit()
ser.close()