Python Forum
Exercise on reading a chunked file of a chess game state
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Exercise on reading a chunked file of a chess game state
#1
Good day,
I am almost completely inexperienced when it comes to python, yet it happens that I need to submit an exercise ASAP wherein I have to fill a template for a method that reads a chunked text file regarding the state of a chess game, and returns said state as an object of Class Game.

The text file is structured as follows:
SHAKKI1205072001CMT54Lauri's rematch, things are going very poorly again...PLR17M5MarkoKa4Ta6b3c3PLR13V5LAURIKd3Rf1END00

Note that ”shakki” means ”chess” in Finnish. ”Musta” is black in Finnish and ”valkoinen” is white, hence the abbreviations ”M” and ”V” in the player chunks (that is, ”M” means black and ”V” means white in the format).

Header : 8 characters
text "SHAKKI" : 6 characters
version number : 2 characters

Date : 8 characters
DDMMYYYY : 8 characters
(DD is a day between 01-31, MM is a month between 01-12, YYYY is a year)

(1 - N) of any chunk type, but so that:
* the last chunk is an END chunk
* there are exactly 2 PLR chunks and they have different colors
Chunks
The structure of chunks is as follows:

Tag : 3 characters (e.g. CMT, PLR, END)
data length : 2 characters (includes a number)
data : (data length) characters
Data length tells us how many characters belong to a chunk, excluding the characters taken by the tag and data length itself. In other words, after reading the number one can safely read that many characters.

Descriptions of the chunks
Comment chunk (tag CMT)
The content of the comment chunk is a string that describes the state of the game.

Player chunk (tag PLR)
In an error-free file there are always two player chunks, one for each color.

Player's color : 1 character (M (black) or V (white))
Name length : 1 character (in range 1-9)
Name : (name length) characters
Player's pieces in chess notation : until the end of the chunk
Note: With the file format, situations where there is an arbitrary number of chess pieces of any type on the board, and the pieces can be located in any square, as long as they do not overlap, can be presented (e.g. chess exercises can be such situations). So, you do not have to worry about the number of chess pieces or whether it is possible to get to a certain situation on the chess board.

End chunk (tag END)
The end chunk’s size is always 00, and it ends the file. So, after reading the end chunk the file can be closed.

Other chunks (unknown tag *** containing any three characters)
Unknown chunks are probably properties added to the format in newer versions. For these chunks, only the size of the chunk is read and the characters of the chunk are then skipped.

Chess notation
The size of a chess board is 8x8. There are 8 rows in the board which are numbered with numbers 1-8, and 8 columns marked with small letters a-h. The board’s corners are thus in the squares a1, a8, h1, and h8.

There are 6 different pieces in chess (in parentheses are the abbreviations that the format uses):

King (K)

Queen (D)

Rook (T)

Bishop (L)

Knight ( R)

Pawn ()

If we want to mark that a king is on the square a3, we write ”Ka3”. A rook in square h8 would be marked as ”Th8”. A pawn has no abbreviation in the notation, so a pawn in square b7 would be marked as ”b7”.

The positions of the pieces in the file are listed consecutively without delimiters. If the first character is a letter between a-h, the piece is a pawn. Otherwise the first character denotes the type of the piece.

Then the given exercise and code:
Implement the following method in the class ChunkIO:

load_game(self, input)

Quote:Creates a new Game object which it returns at the end of the method. The method reads the players and the positions of the pieces using other methods defined in the class. Having read the end chunk, the method returns the Game object to which the players and the board given in the file have been set. The method should skip the comment chunk and possible unknown chunks. When you hopefully test your program, create and close the data stream in your test file, not in this method.

If there is something wrong in the file that is being read, the method should throw a CorruptedChessFileError exception. The given base code for the exercise already throws an exception like this in a situation where there is a problem in reading the data stream. You should throw an exception like this if there is a problem in the structure of the file. Problems like this are e.g. a missing end chunk, a wrong amount of player chunks, one piece on top of another etc.

from game import Game
from corrupted_chess_file_error import *
from player import Player
from piece import Piece
from board import Board


class ChunkIO(object):

    def load_game(self, input):
        """
        @note: This is the game object this method will fill with data. The object
               is returned when the END chunk is reached.
        """
        self.game = Game()

        try:

            # Read the file header and the save date

            header = self.read_fully(8, input)
            date = self.read_fully(8, input)

            # Process the data we just read.
            # NOTE: To test the line below you must test the class once with a broken header

            header = ''.join(header)
            date = ''.join(date)
            if not str(header).startswith("SHAKKI"):
                raise CorruptedChessFileError("Unknown file type")

            # The version information and the date are not used in this
            # exercise

            # *************************************************************
            #
            # EXERCISE
            #
            # ADD CODE HERE FOR READING THE
            # DATA FOLLOWING THE MAIN HEADER
            #
            #
            # *************************************************************


            # If we reach this point the Game object should now have the proper players and
            # a fully set up chess board. Therefore we should return it.

            return self.game

        except OSError:
            # To test this part the stream would have to cause an
            # OSError. That is a bit complicated to test. Therefore we have
            # given you a "secret tool", class BrokenReader, which will throw
            # an OSError at a requested position in the stream.
            # Throw the exception inside any chunk, but not in the chunk header.
            raise CorruptedChessFileError("Reading the chess data failed 1.")




    # HELPER METHODS (you can also implement your own methods here!) -------------------------------------------------------



    def extract_chunk_size(self, chunk_header):
        """
        Given a chunk header (an array of 5 chars) will return the size of this
        chunk's data.

        @param chunk_header:
                   a chunk header to process (str)
        @return: the size (int) of this chunks data
        """
        return int( ''.join( chunk_header[3:5] ) )


    def extract_chunk_name(self, chunk_header):
        """
        Given a chunk header (an array of 5 chars) will return the name of this
        chunk as a 3-letter string.

        @param chunk_header:
                   a chunk header to process
        @return: the name of this chunk
        """
        return ''.join( chunk_header[0:3] )


    def read_fully(self, count, input):
        """
        The read method will occasionally read only some of
        the characters that were requested. This method will repeatedly call read
        to completely fill the given buffer. The size of the buffer tells the
        algorithm how many bytes should be read.

        @param count:
                   How many characters are read
        @param input:
                   The character stream to read from
        @raises: OSError
        @raises: CorruptedChessFileError
        """
        read_chars = input.read( count )

        # If the file end is reached before the buffer is filled
        # an exception is thrown.
        if len(read_chars) != count:
            raise CorruptedChessFileError("Unexpected end of file.")

        return list(read_chars)
Then the following are the classes that are imported in ChunkIO:
from player import Player
from board import Board


class Game(object):
    """
    This class serves as a facade for the chess game.
    """

    def __init__(self):
        # Initialize values
        self._black = None
        self._white = None
        self._board = None


    def get_black(self):
        """
        Returns the black player for this game.
        @return: the black player.
        """
        if self._black:
            return self._black
        raise ValueError("Black player has not been added!")


    def get_white(self):
        """
        Returns the white player for this game.
        @return: the white player.
        """
        if self._white:
            return self._white
        raise ValueError("White player has not been added!");


    def get_board(self):
        """
        Returns the chess board of this game.
        @return: the chess board.
        """
        return self._board
class Player(object):
    """
    This class models a chess player.
    """

    # Color of the player
    WHITE = 0
    BLACK = 1
    

    def __init__(self, name, color):
        """
        Initializes a new player with the given name and color.

        For example :

          player = Player("Matti", Player.BLACK)

        @param name: player's name as a string
        @param color: player's color as a class constant
        """
        # Underscore means that the variable should not be accessed outside this class
        self._name = name
        self._set_color( color )


    def get_name(self):
        """
        Returns the name of the player.

        @return: The name of this player.
        """
        return self._name


    def get_color(self):
        """
        Returns the color of this player.

        @return: the color of this player.
        """
        return self._color


    def _set_color(self, color):
        """
        Sets the player color if it is valid.

        Otherwise raises a ValueError.
        """
        if color in [ Player.WHITE , Player.BLACK]:
            self._color = color
        else:
            raise ValueError("Bad color given")


    # Allow accessing these variables directly, i.e. player.name
    name = property( get_name )
    color = property( get_color )
class Piece(object):
    """
    This class models a single chess piece.
    """

    """
    Class constants listing all possible chess piece types.

    Referring to e.g. the value KING from outside the class Piece
    looks like this:

      Piece.KING

    The letters corresponding to the pieces are:

    King     : K
    Queen    : D
    Rook     : T
    Bishop   : L
    Knight   : R
    Pawn     : (nothing)
    """

    KING = 0
    QUEEN = 1
    BISHOP = 2
    KNIGHT = 3
    ROOK = 4
    PAWN = 5


    def __init__(self, owner, type):
        """
        Initializes a new Piece with the given owner and the given type.

        @param owner: The player who owns this piece
        @param type: The type of this piece
        """
        self._owner = owner    # The player object who owns this piece
        self._set_type(type)   # Type of this piece(KING,...)


    def get_player(self):
        """
        Returns the player who owns this piece.

        @return: the owner (player object) of this piece
        """
        return self._owner


    def get_type(self):
        """
        Returns the type of this piece.

        @return: the type of the piece (KING, QUEEN, BISHOP, KNIGHT, ROOK or PAWN)
        """
        return self._type


    def _set_type(self, type ):
        if type in [ Piece.KING, Piece.QUEEN, Piece.BISHOP, Piece.KNIGHT, Piece.ROOK, Piece.PAWN ]:
            self._type = type
        else:
            raise ValueError("Invalid piece type")


    owner = property( get_player )
    type = property( get_type )
class Board(object):
    """
    This class models a chess board.
    """

    def __init__(self):
        """
        Initiates a new empty board. C{board} is an internal array for holding the chess pieces.
        """
        self._board = [None] * 8
        for i in range(8):
            self._board[i] = [None] * 8


    def set_piece(self, piece, column, row):
        """
        Places a new piece in the given location.

        @param piece: the piece object being placed.
        @param column: the column in which to place the piece
        @param row: the row in which to place the piece
        @raises: ValueError
        """
        if self.is_free(column, row):
            self._board[column][row] = piece
        else:
            # This is an unchecked exception. Therefore it is not marked in the
            # method signature.

            raise ValueError("Tried to place a piece in an occupied square.")


    def get_piece(self, column, row):
        """
        Returns the piece object located in the given coordinates.

        @param column: the column of interest
        @param row: the row of interest
        @return: the piece object located in the coordinates or None if the location was empty
        """
        return self._board[column][row]


    def is_free(self, column, row):
        """
        Tests if the given location was free.

        @param column: the column of interest
        @param row: the row of interest
        @return: true if and only if the location was empty
        """
        return self._board[column][row] == None


    @staticmethod # We can now call this method without an instance
    def column_char_to_integer(column):
        """
        Converts the letters a,b,c,d,e,f,g,h to positions 0-7.
        @param row the character representation of a column.
        @return the integer representation.
        """
        return ord(column) - ord('a')


    @staticmethod # We can now call this method without an instance
    def row_char_to_integer(row):
        """
        Converts the characters '1','2','3'..... to positions 0-7.
        @param row: the character representation of a row.
        @return: the integer representation.
        """
        return ord(row) - ord('1')


    def __getitem__(self, key):
        """ This method allows board to be used like a dictonary.
        E.g. print board['a', '1']

        Note: key should be in column, row -format.
        """
        # Assuming key to be a double (column, row)
        row = self.row_char_to_integer( key[1] )
        column = self.column_char_to_integer( key[0] )
        if 0 <= column < len( self._board ) and 0 <= row < len( self._board[0] ):
            return self.get_piece( column , row )
        else:
            raise KeyError("Invalid key!")


    def __setitem__( self, key, item):
        """ This method allows board to be used like a dictonary.
        E.g. board['a', '1'] = 0.

        Note: key should be in column, row -format.
        """
        # Assuming key to be a double (column, row)
        row = self.row_char_to_integer( key[1] )
        column = self.column_char_to_integer( key[0] )
        if 0 <= column < len( self._board ) and 0 <= row < len( self._board[0] ):
            self.set_piece( item , column , row )
        else:
            raise KeyError("Invalid key!")
And there is an exception class:

class CorruptedChessFileError(Exception):

    def __init__(self, message):
        super(CorruptedChessFileError, self).__init__(message)
I tried running the following code with the exercise grader on the course portal:

class ChunkIO(object):

    def load_game(self, input):
        """
        @note: This is the game object this method will fill with data. The object
               is returned when the END chunk is reached.
        """
        self.game = Game()

        try:

            # Read the file header and the save date

            header = self.read_fully(8, input)
            date = self.read_fully(8, input)

            # Process the data we just read.
            # NOTE: To test the line below you must test the class once with a broken header

            header = ''.join(header)
            date = ''.join(date)
            if not str(header).startswith("SHAKKI"):
                raise CorruptedChessFileError("Unknown file type")

            # The version information and the date are not used in this
            # exercise

            # *************************************************************
            #
            players_read = 0
            while True:
                chunk_header = self.read_fully(5, input)
                chunk_name = self.extract_chunk_name(chunk_header)
                chunk_size = self.extract_chunk_size(chunk_header)

                if chunk_name == "PLR":
                    players_read += 1
                    self.read_player_chunk(chunk_size, input)
                elif chunk_name == "END":
                    if players_read != 2:
                        raise CorruptedChessFileError("There should be exactly 2 player chunks.")
                    break
                else:
                    self.skip_chunk(chunk_size, input)
            #
            #
            # *************************************************************


            # If we reach this point the Game object should now have the proper players and
            # a fully set up chess board. Therefore, we should return it.

            return self.game

        except OSError:
            # To test this part the stream would have to cause an
            # OSError. That is a bit complicated to test. Therefore, we have
            # given you a "secret tool", class BrokenReader, which will throw
            # an OSError at a requested position in the stream.
            # Throw the exception inside any chunk, but not in the chunk header.
            raise CorruptedChessFileError("Reading the chess data failed.")




    # HELPER METHODS (you can also implement your own methods here!) -------------------------------------------------------

    def read_player_chunk(self, chunk_size, input):

        color = self.read_fully(1, input)[0]

        name_length = int(self.read_fully(1, input)[0])
        name = ''.join(self.read_fully(name_length, input))

        if color == 'M':
            player = Player(name, Player.BLACK)
        elif color == 'V':
            player = Player(name, Player.WHITE)
        else:
            raise CorruptedChessFileError(f"Invalid player color: {color}")

        self.game.add_player(player)

        pieces_data = ''.join(self.read_fully(chunk_size - 1 - name_length - 1, input))
        self.piece_position(pieces_data)

    def piece_position(self, pieces_data):
        i = 0
        while i < len(pieces_data):
            piece_char = pieces_data[i]
            position = pieces_data[i + 1:i + 3]

            if piece_char == 'K':
                piece_type = Piece.KING
            elif piece_char == 'D':
                piece_type = Piece.QUEEN
            elif piece_char == 'T':
                piece_type = Piece.ROOK
            elif piece_char == 'L':
                piece_type = Piece.BISHOP
            elif piece_char == 'R':
                piece_type = Piece.KNIGHT
            else:
                piece_type = Piece.PAWN

            column, row = position[0], position[1]
            column_idx = Board.column_char_to_integer(column)
            row_idx = Board.row_char_to_integer(row)

            piece = Piece(self.game.get_white()
                          if self.game.get_white().get_color() == Player.WHITE
                          else self.game.get_black(), piece_type)
            self.game.board.set_piece(piece, column_idx, row_idx)

            i += 3

    def skip_chunk(self, chunk_size, input):
        self.read_fully(chunk_size, input)
(for convenience's sake, the above code is only what I added and edited, not what needs to be left untouched.)

This gives the following error from the graded when tested with any sample text, regardless if it matches what I described at the start or is a broken file:
Error:
Traceback (most recent call last): File "/exercise/tests.py", line 75, in test_01_given_example game = chunk_IO.load_game(self.input_file) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/submission/user/./chunkIO.py", line 45, in load_game self.read_player_chunk(chunk_size, input) File "/submission/user/./chunkIO.py", line 92, in read_player_chunk self.piece_position(pieces_data) File "/submission/user/./chunkIO.py", line 118, in piece_position if self.game.get_white().get_color() == Player.WHITE ^^^^^^^^^^^^^^^^^^^^^ File "/exercise/public/game.py", line 34, in get_white raise ValueError("White player has not been added!"); ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ValueError: White player has not been added!
...and gives the following feedback:
Output:
Tester v8.2.6 English, Python 3.11.1 ======================================================= There was an error in the conversion to a numerical value, the conversion of the numerical type or in the formatting of the output.
As far as I understand, my code doesn't create a proper player object as I have messed up the read_player_chunk method completely, but since my understanding of python is rather dim I can't really know how to implement that correctly. Would like some help ASAP.
Reply
#2
In your class these class variables are defined:

def __init__(self):
        # Initialize values
        self._black = None
        self._white = None
        self._board = None
This function looks for the white player and if that is None, returns the error you get:

 def get_white(self):
        """
        Returns the white player for this game.
        @return: the white player.
        """
        if self._white:
            return self._white
        raise ValueError("White player has not been added!")
So add a white player before you call def get_white(self)!
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  quiz game - problem with loading statements from file lapiduch 2 1,893 Apr-20-2023, 06:13 PM
Last Post: deanhystad
  Excel File reading vanjoe198 1 2,574 Mar-31-2021, 11:53 AM
Last Post: snippsat
  Running my game file yields a different error on a different computer system Bruizeh 0 1,955 Nov-10-2020, 03:15 AM
Last Post: Bruizeh
  reading from a file looseCannon101 14 7,556 Jul-18-2020, 11:29 AM
Last Post: GOTO10
  Weird problem with reading from file and performing calculations pineapple999 1 3,638 Jul-25-2019, 01:30 AM
Last Post: ichabod801
  Handling IO Error / Reading from file Expel 10 6,786 Jul-18-2019, 01:21 PM
Last Post: snippsat
  Reading an Unconventional CSV file OzSbk 2 4,651 May-17-2019, 12:15 PM
Last Post: MvGulik
  reading text file and writing to an output file precedded by line numbers kannan 7 13,431 Dec-11-2018, 02:19 PM
Last Post: ichabod801
  Reading of structured .mat (matlab) file sumit 2 4,191 May-24-2018, 12:12 PM
Last Post: sumit
  File Reading toxicxarrow 9 6,663 May-07-2018, 04:12 PM
Last Post: toxicxarrow

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020