import serial, time, chess, chess.engine, chess.pgn
from tkinter import *
from tkinter import messagebox
from tkinter import simpledialog
from datetime import date
ENGINES = [('Stockfish', '/usr/games/stockfish', {'Skill Level': 7}),
('Ethereal', '/usr/games/ethereal-chess', {}),
('Fruit', '/usr/games/fruit', {}),
('Toga II', '/usr/games/toga2', {}),
('Gnuchess', '/usr/games/gnuchessu', {})]
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.
'''
move_str = board.uci(uci_move)
if move_str == 'e1g1':
pe1 = board.piece_at(chess.E1)
return 'O-O' if pe1.symbol() == 'K' else move_str
elif move_str == 'e1c1':
pe1 = board.piece_at(chess.E1)
return 'O-O-O' if pe1.symbol() == 'K' else move_str
elif move_str == 'e8g8':
pe8 = board.piece_at(chess.E8)
return 'O-O' if pe8.symbol() == 'k' else move_str
elif move_str =='e8c8':
pe8 = board.piece_at(chess.E8)
return 'O-O-O'if pe8.symbol() == 'k' else move_str
elif len(move_str) == 5:
return move_str[:4] + '/' + move_str[4]
else:
return move_str
def send_command(command):
''' Send a command from the PC to the Citrine.
'''
global ser
ser.write((command + '\r\n').encode(encoding='UTF-8'))
time.sleep(0.1)
def save_game():
''' Ask if the user wants to save the game. If so, write the PGN for the
game to Chessgames.txt, or append it to the file if it already exists.
'''
if messagebox.askyesno('Save Game', 'Do you want to save the game?'):
header = engine_option.get() + ' ' + date.today().strftime('%d/%m/%Y')
game = str(chess.pgn.Game().from_board(board)).split('\n')
game = '\n'.join(game[8:]) # Strip empty PGN header.
print(header + '\n' + game)
f = open('ChessGames.txt', 'a')
f.write(header + '\n' + game + '\n\n')
f.close()
def new_game():
''' Initialize a new game.
'''
global board
try:
print(game.from_board(board))
except:
pass
board = chess.Board()
send_command('n') # New Game.
send_command('u on') # Turn Referee Mode on.
send_command('x on') # Turn Xmit on.
send_command('l ea1') # Set the Level.
while True:
line = ser.readline().decode(encoding='UTF-8')[:-2]
if line == '':
break
print('Novag>', line)
GAME_RESULT = ('Draw by repetion', 'Draw by 50 move rule',
'Draw by insufficient material', 'Stalemate',
'Checkmate', 'Computer 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.
global engine_move_now # Start Engine button has just been pressed.
global board
line = ser.readline().decode(encoding='UTF-8')[:-2]
if line != '':
print('Novag>',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)
uci_move = chess.Move.from_uci(mv)
san_move = board.variation_san([uci_move])
if mn != board.fullmove_number or mc != board.turn:
raise ValueError
board.push(uci_move)
update_posn(san_move)
if 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)
uci_move = board.pop()
if mn != board.fullmove_number or mc != board.turn:
raise ValueError
uci_move = board.pop()
san_move = board.variation_san([uci_move])
board.push(uci_move)
update_posn(san_move)
elif line[:8] == 'New Game':
new_game() # User has reset the start position.
save_game()
gui_new_game()
elif engine_move_now:
engine_move_now = False
engine_move()
root.after(100, respond_to_novag)
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 Novag_engine, board, ser, engine_turn
if Novag_engine:
print('Novag Engine Move')
send_command('j')
engine_turn = False
elif board.is_game_over(): # PC engine move and game over.
engine_turn = True
else:
# PC engine move and game ongoing
try:
uci_move = engine.play(board, chess.engine.Limit(time=1.0)).move
nv_move = novag_move(uci_move, board)
print('Pi Engine Move:', nv_move)
if ser.inWaiting() == 0: # Check that there are no take-backs.
send_command('m' + nv_move)
if board.is_capture(uci_move):
time.sleep(3.0)
send_command('m' + nv_move)
engine_turn = False
else:
engine_turn = True
except:
print('Pi Engine failed to respond')
def print_engine_options(eo):
''' Print engine options.
'''
print('Engine Option Type Default Min Max Var')
for k in 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(22) + \
str(eo[k].min).ljust(7) + str(eo[k].max).ljust(8) + \
' '.join([str(v) for v in eo[k].var])
print(option)
def unicode_board(ascii):
''' Return a Chess Alpha font Unicode version of the simple Ascii board.
'''
lightsq_unicode = {'\n': '\n', '.': u'\u0020',
'R': u'\u0072', 'r': u'\u0074', 'N': u'\u0068', 'n': u'\u006A',
'B': u'\u0062', 'b': u'\u006E', 'Q': u'\u0071', 'q': u'\u0077',
'K': u'\u0008', 'k': u'\u006C', 'P': u'\u0070', 'p': u'\u006F'}
darksq_unicode = {'\n': '\n', '.': u'\u002B',
'R': u'\u0052', 'r': u'\u0054', 'N': u'\u0048', 'n': u'\u004A',
'B': u'\u0042', 'b': u'\u004E', 'Q': u'\u0051', 'q': u'\u0057',
'K': u'\u004B', 'k': u'\u004C', 'P': u'\u0050', 'p': u'\u004F'}
unicode = ''
sq_count = 0
for ch in str(ascii):
if ch != ' ':
if sq_count % 2 == 0:
unicode += lightsq_unicode[ch]
else:
unicode += darksq_unicode[ch]
sq_count += 1
return unicode
ENGINE_OPTIONS = ['-- Select an Engine --']
for e in ENGINES:
ENGINE_OPTIONS.append(e[0])
ENGINE_OPTIONS.append('Novag BE8')
for level in range(1,9):
ENGINE_OPTIONS.append('Novag AT' + str(level))
def engine_option_changed(*args):
''' Respond to a change in the engine option by changing the Citrine
playing level.
'''
global ENGINES, ENGINE_OPTIONS, Novag_engine, engine, engine_name
option = engine_option.get()
option_index = ENGINE_OPTIONS.index(option)
if option_index == 0: return
option_menu.config(bg='light gray')
Novag_engine = option_index > len(ENGINES)
if Novag_engine:
send_command('l ' + option[6:].lower())
else:
send_command('l ea1')
engine_name, engine_path, selected_options = ENGINES[option_index - 1]
try:
engine = chess.engine.SimpleEngine.popen_uci(engine_path)
engine.configure(selected_options)
print(engine_name)
print_engine_options(engine.options)
except:
print('Pi engine failed to connect')
def start_stop_button_pressed():
''' Respond to an Engine Start/Stop button press.
'''
global ENGINE_OPTIONS, engine_on, engine_move_now
option = engine_option.get()
option_index = ENGINE_OPTIONS.index(option)
if option_index == 0:
return
if engine_on:
engine_on = False
start_stop_button['text'] = 'Start Engine'
start_stop_button['bg'] = 'light green'
else:
engine_on = engine_move_now = True
start_stop_button['text'] = 'Stop Engine'
start_stop_button['bg'] = 'pink'
def command_button_pressed():
''' Send a user command to the Citrine.
'''
answer = simpledialog.askstring('Citrine Command',
'Type a Citrine command:', parent=root)
if answer: send_command(answer)
def update_posn(san_move):
''' Update the position and last move.
'''
position_label['text'] = unicode_board(board)
move_label['text'] = san_move
root.update()
def gui_new_game():
''' User has reset the pieces on the Novag to start a new game.
'''
engine_on = False
start_stop_button['text'] = 'Start Engine'
start_stop_button['bg'] = 'light green'
update_posn('New Game')
def on_closing():
save_game()
root.destroy()
''' Initalise the serial port, engine and game.
'''
# port = '/dev/ttyUSB0' # With USB serial port adapter.
port = '/dev/ttyS0' # With intergrated serial port.
ser = serial.Serial(port, 57600, timeout=0.05)
print(ser.name, 'opened')
new_game()
engine_on = engine_move_now = False
engine_name = ''
''' Set up the Graphical User Interface.
'''
root = Tk()
root.title('Novag Citrine Interface')
top_frame = Frame(root)
top_frame.pack(pady=6)
engine_option = StringVar()
engine_option.set(ENGINE_OPTIONS[0])
engine_option.trace('w', engine_option_changed)
option_menu = OptionMenu(top_frame, engine_option,
*ENGINE_OPTIONS)
option_menu.config(font=('Dejavu Sans', 10), bg='pink')
menu = top_frame.nametowidget(option_menu.menuname)
menu.config(font=('Dejavu Sans', 10))
option_menu.pack(side='left')
start_stop_button = Button(top_frame, text='Start Engine',
font=('Dejavu Sans', 10), bg='light green',
command=start_stop_button_pressed)
start_stop_button.pack(side='left')
command_button = Button(top_frame, text = u'\u2192 \u25A1',
font=('Dejavu Sans', 10),
command = command_button_pressed)
command_button.pack(side='left')
position_frame = Frame(root, bd=4, relief=GROOVE)
position_frame.pack()
position_label = Label(position_frame, text=unicode_board(board),
font=('Chess Alpha', 30))
position_label.pack()
move_label = Label(root, text='New Game',
font=('Dejavu Sans', 12), pady=6)
move_label.pack()
''' Main loop.
'''
root.protocol("WM_DELETE_WINDOW", on_closing)
respond_to_novag()
root.mainloop()
''' Close down.
'''
try:
if engine_name: engine.quit()
finally:
ser.close()
No comments:
Post a Comment