Skip to content

Commit

Permalink
FIX: final draft of PDN load/save and RCF migration
Browse files Browse the repository at this point in the history
  • Loading branch information
bcorfman committed Nov 13, 2022
1 parent 4d4658a commit 229523d
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 92 deletions.
18 changes: 11 additions & 7 deletions game/gamemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from gui.filelist import FileList
from gui.playercontroller import PlayerController
from gui.alphabetacontroller import AlphaBetaController
from parsing.PDN import PDNReader, PDNWriter
from parsing.migrate import RCF2PDN
from parsing.PDN import PDNReader, PDNWriter, board_to_PDN_ready
from parsing.migrate import RCF2PDN, build_move_annotation_pairs
from util.globalconst import BLACK, WHITE, TITLE, VERSION, KING, MAN, PROGRAM_TITLE, TRAINING_DIR
from util.globalconst import square_map, keymap

Expand Down Expand Up @@ -168,9 +168,9 @@ def open_game(self):

def save_game_as(self):
self._stop_updates()
filename = asksaveasfilename(filetypes=(('Raven Checkers files', '*.rcf'), ('All files', '*.*')),
filename = asksaveasfilename(filetypes=(('Portable Draughts Notation files', '*.pdn'), ('All files', '*.*')),
initialdir=TRAINING_DIR,
defaultextension='.rcf')
defaultextension='.pdn')
if filename == '':
return

Expand Down Expand Up @@ -225,15 +225,19 @@ def _write_file(self, filename):
white_men.append(keymap[i])
elif sq == WHITE | KING:
white_kings.append(keymap[i])
# change description into line comments
description = self.view.serializer.dump()
moves = self.model.curr_state.redo_list
annotations = []
description = '% ' + description
description = description.replace('\n', '\n% ')
board_moves = self.model.curr_state.redo_list
board_orientation = "white_on_top" if self.view.flip_view is False else "black_on_top"
black_player = "Player1"
white_player = "Player2"
move_list, anno_list = board_to_PDN_ready(board_moves)
moves, annotations = build_move_annotation_pairs(move_list, anno_list)
PDNWriter.to_file(filename, '*', '*', datetime.now().strftime("%d/%m/%Y"), '*', black_player, white_player,
to_move, black_men, white_men, black_kings, white_kings, result, board_orientation,
description, moves, annotations)
moves, annotations, description)

# redo moves forward to the previous state
for i in range(undo_steps):
Expand Down
95 changes: 50 additions & 45 deletions parsing/PDN.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import NamedTuple
from base.move import Move
from game.checkers import Checkers
from util.globalconst import keymap, square_map, BLACK, WHITE, MAN, KING
from util.globalconst import keymap, square_map, BLACK, WHITE, MAN, KING, HEADER, DESC, BODY


def is_game_terminator(item):
Expand Down Expand Up @@ -52,16 +52,19 @@ def _removeLineFeed(s):
_CaptureMove = Group(_Square + OneOrMore(Suppress(_CaptureSeparator) + _Square))
_Move = _NormalMove | _CaptureMove
_LineComment = LineStart() + Group('% ' + rest_of_line()('comment'))
_Description = Combine((OneOrMore(Combine(LineStart() + Suppress('% ') + ... + LineEnd()))))('description')
_PDNTag = LineStart() + Suppress('[') + Group(_Identifier('key') + QuotedString('"')('value')) + Suppress(']') + \
Suppress(LineEnd())
_GameHeader = OneOrMore(_PDNTag)
_SingleGameMove = Group(_MoveNumber('number') + _Move('first') + Optional(_Comment('comment1')) + Group(_Result))
_DoubleGameMove = Group(_MoveNumber('number') + _Move('first') + Optional(_Comment('comment1')) +
_Move('second') + Optional(_Comment('comment2')))
_SingleGameMove = Group(_MoveNumber('number') + _Move('first') + Suppress(Optional(_MoveStrength)) +
Optional(_Comment('comment1')) + Group(_Result))
_DoubleGameMove = Group(_MoveNumber('number') + _Move('first') + Suppress(Optional(_MoveStrength)) +
Optional(_Comment('comment1')) + _Move('second') + Suppress(Optional(_MoveStrength)) +
Optional(_Comment('comment2')))
_Variation = Forward()
_GameBody = OneOrMore(_SingleGameMove | _DoubleGameMove | _Variation | _LineComment)('body')
_Variation <<= Combine('(' + _GameBody + ')')
_Game = (_GameHeader('header') + Optional(_GameBody)) | _GameBody
_Game = (_GameHeader('header') + Optional(_Description) + Optional(_GameBody)) | _GameBody


class PDNReader:
Expand All @@ -75,6 +78,7 @@ def __init__(self, stream, source=""):
self._game_titles = []
self._game_indexes = []
self._game_ctr = 0
self._description = ""
self._reset_pdn_vars()

@classmethod
Expand Down Expand Up @@ -245,20 +249,17 @@ def read_game(self, idx):
self._stream.seek(self._game_indexes[idx])
game_chunk = self._game_indexes[idx+1] - self._game_indexes[idx]
pdn = _Game.search_string(self._stream.read(game_chunk))
processed = 0
for game in pdn:
if game.header:
if game.header and not (processed & HEADER):
for tag in game.header:
if parse_header.get(tag.key):
parse_header[tag.key](tag.value)
if game.comment:
self._description += game.comment
else:
if self._black_player and self._white_player:
self._description += f"{self._event}: {self._black_player} vs. {self._white_player}"
else:
self._description += f"{self._event}"
self._description += "\n\nUse the arrow keys in the toolbar above to progress through the game moves."
if game.body:
processed += HEADER
if game.description and not (processed & DESC):
self._description = game.description.as_list().pop()
processed += DESC
if game.body and not (processed & BODY):
self._set_board_defaults_if_needed()
for item in game.body:
if len(item) > 1:
Expand All @@ -280,12 +281,19 @@ def read_game(self, idx):
self._moves.append([move_list, annotation])
else:
raise RuntimeError(f"Cannot interpret item {item} in game.body")
board_moves = self._PDN_to_board(self._next_to_move, self._black_men, self._black_kings,
self._white_men, self._white_kings, self._moves)
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,
self._description, board_moves)
# if no game description was in the file, add a basic one so the user has something to guide them.
if not self._description:
if self._black_player and self._white_player:
self._description += f"{self._event}: {self._black_player} vs. {self._white_player}"
else:
self._description += f"{self._event}"
self._description += "\n\nUse the arrow keys in the toolbar above to progress through the game moves."
board_moves = self._PDN_to_board_ready(self._next_to_move, self._black_men, self._black_kings,
self._white_men, self._white_kings, self._moves)
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,
self._description, board_moves)

def _get_player_to_move(self, turn):
turn = turn.upper()
Expand Down Expand Up @@ -331,24 +339,23 @@ def _try_jump(self, squares: list, annotation: str, state_copy: Checkers):
self._model.make_move(move, state_copy, False, False)
return move

def _PDN_to_board(self, next_to_move: int, black_men: list[int], black_kings: list[int],
white_men: list[int], white_kings: list[int], pdn_moves: list):
def _PDN_to_board_ready(self, next_to_move: int, black_men: list[int], black_kings: list[int],
white_men: list[int], white_kings: list[int], pdn_moves: list):
""" Each move in the file lists the beginning and ending square, along
with an optional annotation string (in Creole fmt) that describes it.
I make sure that each move works on a copy of the model before I commit
to using it inside the code. """
state_copy = copy.deepcopy(self._model.curr_state)
state_copy.clear()
state_copy.to_move = next_to_move
squares = state_copy.squares
for i in black_men:
squares[square_map[i]] = BLACK | MAN
state_copy.squares[square_map[i]] = BLACK | MAN
for i in black_kings:
squares[square_map[i]] = BLACK | KING
state_copy.squares[square_map[i]] = BLACK | KING
for i in white_men:
squares[square_map[i]] = WHITE | MAN
state_copy.squares[square_map[i]] = WHITE | MAN
for i in white_kings:
squares[square_map[i]] = WHITE | KING
state_copy.squares[square_map[i]] = WHITE | KING

# analyze squares to perform a move or jump.
idx = 0
Expand Down Expand Up @@ -422,7 +429,7 @@ def _translate_to_text(move):
item.reverse()
anno.reverse()
movenum += 1
# use a pipe as a temporary delimiter so TextWrapper will treat each numbered move
# use a backquote as a temporary delimiter so TextWrapper will treat each numbered move
# as a single item for wrapping. After the wrapping is done, the pipe characters
# will be replaced with spaces.
move1 = item.pop()
Expand Down Expand Up @@ -487,8 +494,7 @@ def _write(self, event: str, site: str, date: str, rnd: str, black_player: str,
self.stream.write(f'[FEN "{fen}"]\n')
self.stream.write(f'[BoardOrientation "{board_orientation}"]\n')
if description:
for line in description:
self.stream.write(line)
self.stream.write(description + "\n")
if annotations is None:
annotations = [["", ""] for _ in moves]
for line in self._wrapper.wrap(_translate_to_movetext(moves, annotations)):
Expand Down Expand Up @@ -517,30 +523,29 @@ def to_stream(cls, stream, event, site, date, rnd, black_player, white_player, n
white_kings, result, board_orientation, description, moves, annotations)


def board_to_PDN(board_moves: list[Move]):
pdn_moves = ""
move_count = 1
def board_to_PDN_ready(board_moves: list[Move]):
pdn_moves = []
annotations = []
for idx, move in enumerate(board_moves):
num_squares = len(move.affected_squares)
if idx % 2 == 0:
pdn_moves += f"{move_count}. "
move_count += 1
if num_squares == 2: # move
sq1 = keymap[move.affected_squares[0][0]]
sq2 = keymap[move.affected_squares[1][0]]
pdn_moves += f"{sq1}-{sq2}"
pdn_moves.append([sq1, sq2])
elif num_squares >= 3: # jump
jump = []
for i in range(0, num_squares-2, 2):
sq = keymap[move.affected_squares[i][0]]
pdn_moves += f"{sq}x"
jump.append(sq)
sq = keymap[move.affected_squares[-1][0]]
pdn_moves += f"{sq}"
jump.append(sq)
pdn_moves.append(jump)
else:
raise RuntimeError("unknown number of affected_squares")
if move.annotation:
pdn_moves += " {" + f"{move.annotation}" + "}"
if idx % 2 == 1:
pdn_moves += " "
annotations.append(move.annotation)
else:
pdn_moves += " "
return pdn_moves.rstrip()
annotations.append("")
pdn_moves.reverse()
annotations.reverse()
return pdn_moves, annotations
61 changes: 33 additions & 28 deletions parsing/migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,37 @@ def _get_game_result(_anno):
return game_result


def build_move_annotation_pairs(move_list, anno_list):
# populate real moves list with move pairs
move_collection = []
anno_collection = []
move_pair = []
anno_pair = []
i = 0
moves = list(reversed(move_list))
annotations = list(reversed(anno_list))
result = None
while moves:
move = moves.pop()
anno = annotations.pop()
result = _get_game_result(anno)
move_pair.append(move)
anno_pair.append(anno)
i += 1
if i % 2 == 0:
move_collection.append(move_pair[:])
anno_collection.append(anno_pair[:])
move_pair = []
anno_pair = []
if result:
move_pair.append(result)
anno_pair.append("")
if move_pair:
move_collection.append(move_pair[:])
anno_collection.append(anno_pair[:])
return move_collection, anno_collection


class RCF2PDN:
def __init__(self):
self.description = []
Expand Down Expand Up @@ -87,7 +118,7 @@ def _transform_input(self):
orientation = "black_on_top" if self.flip_board == 1 else "white_on_top"
description = ""
for line in self.description:
description += f"; {line}"
description += f"% {line}"
self._game = Game(event, site, date, rnd, black_player, white_player, self.next_to_move, list(self.black_men),
list(self.white_men), list(self.black_kings), list(self.white_kings), result,
orientation, description, self.moves)
Expand Down Expand Up @@ -121,8 +152,6 @@ def _read_description(self, stream):
stream.seek(prior_loc)
self.lineno -= 1
break
elif line == '\n':
continue
else:
self.description.append(line)

Expand Down Expand Up @@ -168,31 +197,7 @@ def _read_moves(self, stream):
move_list = [int(sq) for sq in move.split('-')]
moves.append(move_list)
annotations.append(annotation)
# populate real moves list with move pairs
move_pair = []
annotation_pair = []
i = 0
moves.reverse()
annotations.reverse()
result = None
while moves:
move = moves.pop()
anno = annotations.pop()
result = _get_game_result(anno)
move_pair.append(move)
annotation_pair.append(anno)
i += 1
if i % 2 == 0:
self.moves.append(move_pair[:])
self.annotations.append(annotation_pair[:])
move_pair = []
annotation_pair = []
if result:
move_pair.append(result)
annotation_pair.append("")
if move_pair:
self.moves.append(move_pair[:])
self.annotations.append(annotation_pair[:])
self.moves, self.annotations = build_move_annotation_pairs(moves, annotations)

def _read_turn(self, line):
self.next_to_move = line.split("_")[0].lower()
Expand Down
25 changes: 16 additions & 9 deletions tests/test_PDN.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import os
from base.move import Move
from parsing.PDN import PDNReader, PDNWriter, translate_to_fen, board_to_PDN
from parsing.PDN import PDNReader, PDNWriter, translate_to_fen, board_to_PDN_ready
from util.globalconst import BLACK, WHITE, MAN, KING, FREE, square_map


def test_board_to_PDN():
def test_board_to_PDN_ready():
board_moves = [Move([[23, BLACK | MAN, FREE], [29, WHITE | MAN, FREE], [35, FREE, FREE], [40, WHITE | MAN, FREE],
[45, FREE, BLACK | KING]],
'Black double jump taking out two white men and landing in the king row')]
pdn_moves = '1. 16x23x32 {Black double jump taking out two white men and landing in the king row}'
assert board_to_PDN(board_moves) == pdn_moves
pdn_moves, pdn_annotations = [[16, 23, 32]], \
['Black double jump taking out two white men and landing in the king row']
moves, annotations = board_to_PDN_ready(board_moves)
assert moves == pdn_moves
assert annotations == pdn_annotations
# First moves from Ballot 1, trunk line in Complete Checkers by Richard Pask
board_moves = [Move([[square_map[9], BLACK | MAN, FREE], [square_map[13], FREE, BLACK | MAN]], ""),
Move([[square_map[21], WHITE | MAN, FREE], [square_map[17], FREE, WHITE | MAN]], ""),
Expand Down Expand Up @@ -41,11 +44,15 @@ def test_board_to_PDN():
Move([[square_map[32], WHITE | MAN, FREE], [square_map[27], FREE, WHITE | MAN]], ""),
Move([[square_map[8], BLACK | MAN, FREE], [square_map[12], FREE, BLACK | MAN]], ""),
Move([[square_map[27], WHITE | MAN, FREE], [square_map[24], FREE, WHITE | MAN]], "")]
pdn_moves = '1. 9-13 21-17 2. 5-9 25-21 3. 11-15 29-25 4. 9-14 23-18 5. 14x23 27x18x11 '\
'6. 8x15 17-14 7. 10x17 21x14 8. 12-16 24-20 {26-23; 16-19 23-16; 7-11 16-7 '\
'3-26 30-23; 4-8 25-22; 8-11 24-19; 15-24 28-19; 6-10 to a draw} 9. 16-19 25-21 '\
'{32-27; 4-8 25-21 same} 10. 4-8 32-27 11. 8-12 27-24'
assert board_to_PDN(board_moves) == pdn_moves
pdn_moves = [[27, 24], [8, 12], [32, 27], [4, 8], [25, 21], [16, 19], [24, 20], [12, 16], [21, 14], [10, 17],
[17, 14], [8, 15], [27, 18, 11], [14, 23], [23, 18], [9, 14], [29, 25], [11, 15], [25, 21], [5, 9],
[21, 17], [9, 13]]
pdn_annotations = ['', '', '', '', '32-27; 4-8 25-21 same', '',
'26-23; 16-19 23-16; 7-11 16-7 3-26 30-23; 4-8 25-22; 8-11 24-19; 15-24 28-19; 6-10 to a draw',
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
moves, annotations = board_to_PDN_ready(board_moves)
assert moves == pdn_moves
assert annotations == pdn_annotations


def test_parse_PDN_string_success():
Expand Down
Loading

0 comments on commit 229523d

Please sign in to comment.