diff --git a/.flake8 b/.flake8 index de2d545..e1ad76b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,7 @@ [flake8] exclude = __pypackages__ + .pytest_cache + .github + .venv + .vscode diff --git a/game/gamemanager.py b/game/gamemanager.py index dedc712..fc10866 100644 --- a/game/gamemanager.py +++ b/game/gamemanager.py @@ -2,13 +2,14 @@ from tkinter.filedialog import askopenfilename, asksaveasfilename from tkinter.messagebox import askyesnocancel, showerror from util.globalconst import BLACK, WHITE, TITLE, VERSION, KING, MAN, PROGRAM_TITLE, TRAINING_DIR -from util.globalconst import square_map, keymap +from util.globalconst import keymap from game.checkers import Checkers from gui.boardview import BoardView +from gui.filelist import FileList from gui.playercontroller import PlayerController from gui.alphabetacontroller import AlphaBetaController from parsing.gamepersist import SavedGame -from parsing. +from parsing.PDN import PDNReader class GameManager(object): @@ -34,11 +35,11 @@ def set_controllers(self): if self.num_players == 0: self.controller1 = AlphaBetaController(model=self.model, view=self.view, - searchtime=think_time, + searchtime=self.think_time, end_turn_event=self.turn_finished) self.controller2 = AlphaBetaController(model=self.model, view=self.view, - searchtime=think_time, + searchtime=self.think_time, end_turn_event=self.turn_finished) elif self.num_players == 1: # assumption here is that Black is the player @@ -47,7 +48,7 @@ def set_controllers(self): end_turn_event=self.turn_finished) self.controller2 = AlphaBetaController(model=self.model, view=self.view, - searchtime=think_time, + searchtime=self.think_time, end_turn_event=self.turn_finished) # swap controllers if White is selected as the player if self.player_color == WHITE: @@ -101,31 +102,35 @@ def load_game(self, filename): self._stop_updates() try: reader = PDNReader.from_file(filename) - # saved_game = SavedGame() - # saved_game.read(filename) - self.model.curr_state.clear() - self.model.curr_state.to_move = saved_game.to_move - self.num_players = saved_game.num_players + game_list = reader.get_game_list() + if len(game_list) > 1: + FileList(self._root, game_list) + else: + game = reader.read_game(0) + print(game) + # self.model.curr_state.clear() + # self.model.curr_state.to_move = saved_game.to_move + # self.num_players = saved_game.num_players # this section will work - squares = self.model.curr_state.squares - for i in saved_game.black_men: - squares[square_map[i]] = BLACK | MAN - for i in saved_game.black_kings: - squares[square_map[i]] = BLACK | KING - for i in saved_game.white_men: - squares[square_map[i]] = WHITE | MAN - for i in saved_game.white_kings: - squares[square_map[i]] = WHITE | KING - self.model.curr_state.reset_undo() - self.model.curr_state.redo_list = saved_game.moves - self.model.curr_state.update_piece_count() - self.view.reset_view(self.model) - self.view.serializer.restore(saved_game.description) - self.view.curr_annotation = self.view.get_annotation() - self.view.flip_board(saved_game.flip_board) - self.view.update_statusbar() - self.parent.set_title_bar_filename(filename) - self.filename = filename + # squares = self.model.curr_state.squares + # for i in saved_game.black_men: + # squares[square_map[i]] = BLACK | MAN + # for i in saved_game.black_kings: + # squares[square_map[i]] = BLACK | KING + # for i in saved_game.white_men: + # squares[square_map[i]] = WHITE | MAN + # for i in saved_game.white_kings: + # squares[square_map[i]] = WHITE | KING + # self.model.curr_state.reset_undo() + # self.model.curr_state.redo_list = saved_game.moves + # self.model.curr_state.update_piece_count() + # self.view.reset_view(self.model) + # self.view.serializer.restore(saved_game.description) + # self.view.curr_annotation = self.view.get_annotation() + # self.view.flip_board(saved_game.flip_board) + # self.view.update_statusbar() + # self.parent.set_title_bar_filename(filename) + # self.filename = filename except IOError as err: showerror(PROGRAM_TITLE, 'Invalid file. ' + str(err)) @@ -185,7 +190,7 @@ def _write_file(self, filename): saved_game.white_men.append(keymap[i]) elif sq == WHITE | KING: saved_game.white_kings.append(keymap[i]) - saved_game.description = self.view.serializer.dump() + # saved_game.description = self.view.serializer.dump() saved_game.moves = self.model.curr_state.redo_list saved_game.flip_board = self.view.flip_view saved_game.write(filename) diff --git a/gui/aboutbox.py b/gui/aboutbox.py index 5d9120c..146d7f6 100644 --- a/gui/aboutbox.py +++ b/gui/aboutbox.py @@ -4,14 +4,14 @@ class AboutBox(Dialog): - def __init__(self, parent): - Dialog.__init__(self, parent) + def __init__(self, parent, title): + Dialog.__init__(self, parent, title) self.canvas = None self.button = None self.blank = None def body(self, master): - self.canvas = Canvas(self, width=300, height=275) + self.canvas = Canvas(self, width=300, height=275, bg='light gray') self.canvas.pack(side=TOP, fill=BOTH, expand=0) self.canvas.create_text(152, 47, text='Raven', fill='black', font=('Helvetica', 36)) @@ -19,34 +19,34 @@ def body(self, master): font=('Helvetica', 36)) self.canvas.create_text(150, 85, text='Version ' + VERSION, fill='black', - font=('Helvetica', 12)) + font=('Helvetica', 14)) self.canvas.create_text(150, 115, text='An open source checkers program', fill='black', - font=('Helvetica', 10)) + font=('Helvetica', 12)) self.canvas.create_text(150, 130, text='by Brandon Corfman', fill='black', - font=('Helvetica', 10)) + font=('Helvetica', 12)) self.canvas.create_text(150, 160, text='Evaluation function translated from', fill='black', - font=('Helvetica', 10)) + font=('Helvetica', 12)) self.canvas.create_text(150, 175, text="Martin Fierz's Simple Checkers", fill='black', - font=('Helvetica', 10)) + font=('Helvetica', 12)) self.canvas.create_text(150, 205, text="Alpha-beta search code written by", fill='black', - font=('Helvetica', 10)) + font=('Helvetica', 12)) self.canvas.create_text(150, 220, text="Peter Norvig for the AIMA project;", fill='black', - font=('Helvetica', 10)) + font=('Helvetica', 12)) self.canvas.create_text(150, 235, text="adopted for checkers usage", fill='black', - font=('Helvetica', 10)) + font=('Helvetica', 12)) self.canvas.create_text(150, 250, text="by Brandon Corfman", fill='black', - font=('Helvetica', 10)) + font=('Helvetica', 12)) return self.canvas - def cancel(self, event=None): + def cancel(self, _=None): self.destroy() def buttonbox(self): diff --git a/gui/boardview.py b/gui/boardview.py index 321e4c8..96b6e81 100644 --- a/gui/boardview.py +++ b/gui/boardview.py @@ -372,7 +372,7 @@ def notify(self, move): def get_annotation(self): # return self.serializer.dump() - pass + return "" def erase_checker(self, index): self.canvas.delete('c'+str(index)) diff --git a/gui/filelist.py b/gui/filelist.py new file mode 100644 index 0000000..7947114 --- /dev/null +++ b/gui/filelist.py @@ -0,0 +1,44 @@ +from tkinter import Button, Frame, Listbox, Variable, TOP +from tkinter.simpledialog import Dialog +from gui.autoscrollbar import AutoScrollbar + + +class FileList(Dialog): + def __init__(self, parent, game_titles): + self._master = parent + self.filelist = None + self.scrollbar = None + self.ok_button = None + self.blank = None + self.result = False + self._titles = tuple(item.name for item in game_titles) + Dialog.__init__(self, self._master, "Select game title") + + def body(self, master): + var = Variable(value=self._titles) + panel = Frame(self, borderwidth=1, relief='sunken') + self.scrollbar = AutoScrollbar(self, container=panel, + row=1, column=1, sticky='ns') + self.filelist = Listbox(self, width=40, height=20, listvariable=var, yscrollcommand=self.scrollbar.set) + self.filelist.pack(side=TOP) + self.scrollbar.config(command=self.filelist.yview) + panel.pack(side='top', fill='both', expand=True) + self.filelist.grid(in_=panel, row=1, column=0, sticky='nsew') + panel.grid_rowconfigure(1, weight=1) + panel.grid_columnconfigure(0, weight=1) + + def apply(self): + self.result = True + + def cancel(self, _=None): + if self._master is not None: + self._master.focus_set() + self.destroy() + + def buttonbox(self): + self.ok_button = Button(self, text='OK', width=5, command=self.apply) + self.ok_button.pack(side="left") + cancel_button = Button(self, text='Cancel', width=5, command=self.cancel) + cancel_button.pack(side="right") + self.bind("", lambda event: self.apply()) + self.bind("", lambda event: self.cancel()) diff --git a/gui/prefdlg.py b/gui/prefdlg.py index d14e0d2..55f5d79 100644 --- a/gui/prefdlg.py +++ b/gui/prefdlg.py @@ -10,6 +10,14 @@ def __init__(self, parent, title, font, size): self.result = False self.font = font self.size = size + self._npFrame = None + self._fontFrame = None + self._fontLabel = None + self._fontCombo = None + self._sizeCombo = None + self._sizeFrame = None + self._sizeLabel = None + self.parent = None Dialog.__init__(self, parent, title) def body(self, master): @@ -24,8 +32,7 @@ def body(self, master): self._sizeFrame = Frame(self._npFrame, borderwidth=0) self._sizeLabel = Label(self._sizeFrame, text='Size:', width=5) self._sizeLabel.pack(side=LEFT, padx=3) - self._sizeCombo = Combobox(self._sizeFrame, values=range(8, 15), - state='readonly') + self._sizeCombo = Combobox(self._sizeFrame, values=list(range(8, 15)), state='readonly') self._sizeCombo.pack(side=RIGHT, fill=X) self._fontFrame.pack() self._sizeFrame.pack() @@ -38,7 +45,7 @@ def apply(self): self.size = self._sizeCombo.get() self.result = True - def cancel(self, event=None): + def cancel(self, _=None): if self.parent is not None: self.parent.focus_set() self.destroy() diff --git a/main.py b/main.py index 47d5685..46d755c 100644 --- a/main.py +++ b/main.py @@ -95,7 +95,7 @@ def create_game_menu(self): game.add_separator() game.add_command(label='Exit', underline=0, command=self._on_close) - self.menu_bar.add_cascade(label='_Game', menu=game) + self.menu_bar.add_cascade(label='Game', menu=game) def create_options_menu(self): options = Menu(self.menu_bar, tearoff=0) diff --git a/parsing/PDN.py b/parsing/PDN.py index 004ef45..d2b4202 100644 --- a/parsing/PDN.py +++ b/parsing/PDN.py @@ -61,9 +61,12 @@ class PDNReader: def __init__(self, stream, source=""): self._stream = stream self._source = f"in {source}" if source else "" + self._stream_pos = 0 self._lineno = 0 self._games = [] self._game_titles = [] + self._game_indexes = [] + self._game_ctr = 0 self._reset_pdn_vars() @classmethod @@ -74,8 +77,10 @@ def from_string(cls, pdn_string): @classmethod def from_file(cls, filepath): filename = os.path.basename(filepath) + # sample a small chunk of the file to determine encoding + chunk_size = min(os.path.getsize(filepath), 10000) with open(filepath, 'rb') as test: - pdn_encoding = charset_normalizer.detect(test.read())['encoding'] + pdn_encoding = charset_normalizer.detect(test.read(chunk_size))['encoding'] stream = open(filepath, encoding=pdn_encoding) return cls(stream, filename) @@ -108,8 +113,6 @@ def _reset_pdn_vars(self): self._description = "" self._moves = [] self._annotations = [] - self._game_idx = 0 - self._game_titles = [] def _read_event(self, value): self._event = value @@ -154,8 +157,8 @@ def _start_move_list(self, _): title = f"{self._event}: {self._black_player} vs. {self._white_player}" else: title = f"{self._event}" - self._game_titles.append(GameTitle(index=self._game_idx, name=title)) - self._game_idx += 1 + self._game_titles.append(GameTitle(index=self._game_ctr, name=title)) + self._game_ctr += 1 def _read_fen(self, value): turn, first_squares, second_squares = value.split(":") @@ -175,43 +178,63 @@ def _read_fen(self, value): else: raise SyntaxError("Unknown player type {player} in second set of FEN squares") + def _add_game_index(self, value): + self._event = value + self._game_indexes.append(self._stream_pos) + def get_game_list(self): - parse_header = {"[Event": self._read_event, + self._game_ctr = 0 + self._stream.seek(0) + parse_header = {"[Event": self._add_game_index, "[White": self._read_white_player, "[Black": self._read_black_player, "1.": self._start_move_list, } self._game_titles = [] + self._game_indexes = [] while True: + self._stream_pos = self._stream.tell() line = self._stream.readline() if line == "": + self._game_indexes.append(self._stream.tell()) break for key in parse_header: - if line.lstrip().startswith(key): - parse_header[key](line) + if line.startswith(key): + line = line.lstrip() + value = line.split('"')[1] if line.startswith("[") else line + parse_header[key](value) break - self._stream.seek(0) return self._game_titles def read_game(self, idx): - # read up to the requested game index - if idx > 0: - num_games = 0 - line = self._stream.readline() - if line == "" or idx < num_games: - raise RuntimeError(f"Cannot find game number {idx}") - - while True: - prior_loc = self._stream.tell() + prior_game = 0 + next_event = None + if not self._game_indexes: + prior_game = self._stream.seek(0) + # read up to the requested game index + if idx > 0: + num_games = 0 line = self._stream.readline() - if line == "": + if line == "" or idx < num_games: raise RuntimeError(f"Cannot find game number {idx}") - if line.lstrip().startswith("[Event"): - num_games += 1 - if idx == num_games: - self._stream.seek(prior_loc) - break + + while True: + line = self._stream.readline() + if line == "": + if num_games == idx: + self._stream.seek(prior_game) + else: + raise RuntimeError(f"Cannot find game number {idx}") + if line.startswith("[Event"): + num_games += 1 + if idx + 1 == num_games: + prior_game = self._stream_pos + self._stream.seek(prior_game) + break + + # TODO: find [Event index of next game to create game chunk (size) + next_event = 14352495435 # parse the game at the requested index parse_header = {"Event": self._read_event, @@ -226,7 +249,12 @@ def read_game(self, idx): "FEN": self._read_fen, "BoardOrientation": self._read_board_orientation} - pdn = _Game.search_string(self._stream.read()) + if self._game_indexes: + self._stream.seek(self._game_indexes[idx]) + game_chunk = self._game_indexes[idx+1] - self._game_indexes[idx] + else: + game_chunk = next_event - prior_game + pdn = _Game.search_string(self._stream.read(game_chunk)) for game in pdn: if game.header: for tag in game.header: @@ -254,7 +282,6 @@ def read_game(self, idx): annotations.append("") self._moves.append(moves) self._annotations.append(annotations) - return Game(self._event, self._site, self._date, self._round, self._black_player, self._white_player, self._next_to_move, self._black_men, self._white_men, self._black_kings, self._white_kings, self._result, self._flip_board, @@ -310,7 +337,7 @@ def _translate_to_fen(next_to_move, black_men, white_men, black_kings, white_kin return fen -def _translate_to_movetext(moves: list, annotations: list, result: str): +def _translate_to_movetext(moves: list, annotations: list): def _translate_to_text(move): sq1, sq2 = move[0], move[1] sep = '-' if abs(sq1 - sq2) <= 5 else 'x' @@ -399,7 +426,7 @@ def _write(self, event: str, site: str, date: str, rnd: str, black_player: str, if description: for line in description: self.stream.write(line) - for line in self._wrapper.wrap(_translate_to_movetext(moves, annotations, result)): + for line in self._wrapper.wrap(_translate_to_movetext(moves, annotations)): line = line.replace("`", " ") # NOTE: see _translate_to_movetext function self.stream.write(line + '\n') diff --git a/tests/test_PDN.py b/tests/test_PDN.py index e219010..f97723c 100644 --- a/tests/test_PDN.py +++ b/tests/test_PDN.py @@ -61,9 +61,15 @@ def test_parse_PDN_string_success(): def test_parse_PDN_file_success(): pdn_file = os.path.join('training', 'OCA_2.0.pdn') with PDNReader.from_file(pdn_file) as reader: - assert len(reader.get_game_list()) == 22621 + game_list = reader.get_game_list() + assert len(game_list) == 22621 + assert game_list[5].name == "Edinburgh 1847, game 2: Anderson, A. vs. Wyllie, J." game = reader.read_game(22620) assert game.event == "German Open 2004" + game = reader.read_game(5) + assert game.event == "Edinburgh 1847, game 2" + game = reader.read_game(0) + assert game.event == "Manchester 1841" def test_write_PDN_file_success(tmp_path):