
def auto_doc(cls):
    import inspect

    methods = [name for name, member in inspect.getmembers(cls, inspect.isfunction)]
    regular_methods = [name for name in methods if not (name.startswith('__') and name.endswith('__'))]

    cls.__doc__ = f"""
    {cls.__name__} - Auto-documented class

    Public methods ({len(regular_methods)}):
    {', '.join(sorted(regular_methods))}

    Original docstring:
    {cls.__doc__ or 'No description provided.'}
    """
    return cls

@auto_doc
class Board:

    """
    Class Board, provides grid-based 2D rectangular board instance made by two-layered lists.
    Perfect for board game creation.
    """

    def __init__(self, width, height, item=None, background=None, joint=""):
        if not isinstance(width, int) or not isinstance(height, int): raise TypeError("width and height must be integers")
        if width <= 0 or height <= 0: raise ValueError("width and height must be positive integers, excluding zeros")
        self.background = background
        self.body = [[item for _ in range(width)] for _ in range(height)]
        self.joint = joint

    def __str__(self): return "\n".join([self.joint.join(str(item) for item in row) for row in self.body])

    def __repr__(self): return repr(self.body)

    __hash__ = None

    def __iter__(self):
        """
        Makes Board iterable.
        Yield (row, col, item).
        """
        for row in range(self.height):
            for col in range(self.width): yield (row, col, self.body[row][col])

    def __eq__(self, other):
        if not isinstance(other, Board): raise TypeError("other must be Board object")
        return self.body == other.body and self.background == other.background

    def __ne__(self, other): return not self.__eq__(other)

    def __add__(self, other): return NotImplemented
    def __sub__(self, other): return NotImplemented
    def __mul__(self, other): return NotImplemented
    def __truediv__(self, other): return NotImplemented
    def __floordiv__(self, other): return NotImplemented
    def __mod__(self, other): return NotImplemented
    def __pow__(self, other): return NotImplemented
    def __radd__(self, other): return NotImplemented
    def __rsub__(self, other): return NotImplemented
    def __rmul__(self, other): return NotImplemented
    def __rtruediv__(self, other): return NotImplemented
    def __rfloordiv__(self, other): return NotImplemented
    def __rmod__(self, other): return NotImplemented
    def __rpow__(self, other): return NotImplemented

    __add__.__doc__ = __sub__.__doc__ = __mul__.__doc__ = \
    __truediv__.__doc__ = __floordiv__.__doc__ = __mod__.__doc__ = \
    __pow__.__doc__ = __radd__.__doc__ = __rsub__.__doc__ = \
    __rmul__.__doc__ = __rtruediv__.__doc__ = __rfloordiv__.__doc__ = \
    __rmod__.__doc__ = __rpow__.__doc__ =  "Return NotImplemented."

    def __getitem__(self, index):
        """
        Always return a list or a 2D list.
        Board object accepts tuple indexing, while lists do not.
        Therefore, be careful of the type.
        Note:
            The code for this function might seem over-complicated and strange,
            but it is actually for this:
            when self.body = [
                [1, 2, 3],
                [4, 5, 6],
                [7, 8, 9],
            ]
            self[0:3, 0] returns [1, 4, 7] instead of [1, 2, 3].
        """
        if isinstance(index, tuple):
            new = [item[index[1]] for item in self.body[index[0]] if isinstance(self.body[index[0]][0], list)]
            if not new: new = self.body[index[0]][index[1]]
            return new
        else:
            return self.body[index]

    def __setitem__(self, index, value):
        """Set value at position index."""
        if isinstance(index, tuple):
            row, col = index
            self.body[row][col] = value
        else:
            self.body[index] = value

    @property
    def width(self):
        """Return the width of the board."""
        return len(self.body[0])

    @property
    def height(self):
        """Return the height of the board."""
        return len(self.body)

    @property
    def size(self):
        """Return the size, width * height, of ther board."""
        return self.width * self.height

    @property
    def steps(self):
        """Return a list of (r, c) where r is all the rows and c is all the cols in the row."""
        return [(r, c) for r in range(self.height) for c in range(self.width)]

    def extend_to_top(self, other):
        """Extend Board to the top with another Board object. Return None."""
        if not isinstance(other, Board): raise TypeError("other must be Board object")
        if self.width != other.width: raise ValueError("board widths must be equal")
        for row in other.body: self.body.insert(0, row)

    def extend_to_bottom(self, other):
        """Extend Board to the bottom with another Board object. Return None."""
        if not isinstance(other, Board): raise TypeError("other must be Board object")
        if self.width != other.width: raise ValueError("board widths must be equal")
        for row in other.body: self.body.append(row)

    def extend_to_left(self, other):
        """Extend Board to the left with another Board object. Return None."""
        if not isinstance(other, Board): raise TypeError("other must be Board object")
        if self.height != other.height: raise ValueError("board heights must be equal")
        for row in range(self.height): self.body[row][:0] = other.body[row]

    def extend_to_right(self, other):
        """Extend Board to the right with another Board object. Return None."""
        if not isinstance(other, Board): raise TypeError("other must be Board object")
        if self.height != other.height: raise ValueError("board heights must be equal")
        for row in range(self.height): self.body[row].extend(other.body[row])

    def rotate(self):
        """Rotate Board by 90 degrees clockwise everytime called. Return None."""
        self.body = [[self.body[::-1][r][c] for r in range(self.height)] for c in range(self.width)]

    def compress(self):
        """Compress Board to the smallest possible size, remaining the non-background items. Return None."""
        while self.body and all(cell == self.background for cell in self[0]):
            self.body.pop(0)
        while self.body and all(cell == self.background for cell in self[-1]):
            self.body.pop()
        while self.body and all(cell == self.background for cell in self[:, 0]):
            for row in self.body: row.pop(0)
        while self.body and all(cell == self.background for cell in self[:, -1]):
            for row in self.body: row.pop()

    def overlaps(self, other):
        """Return list of (row, col) of the positions where both Board objects overlap with non-background cells."""
        if other.width != self.width or other.height != self.height: raise ValueError("Board size must be exactly the same.")
        overlaps = []
        for r, c, item in self:
            if item != self.background and other[r][c] != other.background:
                overlaps.append((r, c))
        return overlaps

    def collides(self, other):
        """Return True if any non-background cells overlap."""
        if other.width != self.width or other.height != self.height: raise ValueError("Board size must be exactly the same.")
        for r, c, item in self:
            if item != self.background and other[r][c] != other.background: return True
        return False

    def join_all(self):
        """Join all rows into one long row"""
        self.body = [[cell for row in self.body for cell in row]]

    def binary_mark(self, code):
        """Fill board with binary digits of code, padded with leading zeros."""
        code = bin(code)[2:].zfill(self.width*self.height)
        i = 0
        for r, c, _ in self:
            self.body[r][c] = int(code[i])
            i += 1

    def connected(self, row, col):
        """Return all cells connected (4-direction, NESW) to (row,col) with same value."""
        item = self[row, col]
        connections = {(row, col, item)}
        while True:
            before = len(connections)
            for r, c, val in list(connections):
                for dr, dc in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < self.height and 0 <= nc < self.width:
                        if self[nr, nc] == item: connections.add((nr, nc, item))
            if len(connections) == before: break
        return [(r, c) for r, c, _ in connections]

    def copy(self):
        """Return a deep copy of the board."""
        new_board = Board(self.width, self.height, background=self.background, joint=self.joint)
        new_board.body = [row[:] for row in self.body]
        return new_board

    def fill(self, item):
        """Fill entire board with item. Return None."""
        for r, c in self.steps: self[r, c] = item

    def clear(self):
        """Clear board to background. Return None."""
        self.fill(self.background)

    def find_all(self, item):
        """Find all occurences of item and returns a list with (row, col) of the positions"""
        return [(r, c) for r, c, val in self if val == item]

    def count(self, item):
        """Count the ocurrences of an item in a Board"""
        return sum([1 for _, _, val in self if val == item])

    def hflip(self):
        """Flip board horizontally (mirror left-right). Return None."""
        for row in self.body:
            row.reverse()

    def vflip(self):
        """Flip board vertically (mirror top-bottom). Return None."""
        self.body.reverse()

    def random_fill(self):
        """Randomly fill board with random items from ASCII character no.32 to no.126. Return None."""
        import random
        characters = [chr(i) for i in range(32, 127)]
        for r, c in self.steps:
            self[r, c] = random.choice(characters)

    def is_on_border(self, row, col):
        """Check if cell is on board border."""
        return row == 0 or col == 0 or row == self.height-1 or col == self.width-1

    def neighbors(self, row, col, include_diagonal=False):
        """Return list of (r, c) of neighboring cells."""
        dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]
        if include_diagonal: dirs += [(1, 1), (1, -1), (-1, 1), (-1, -1)]
        result = []
        for dr, dc in dirs:
            nr, nc = row + dr, col + dc
            if 0 <= nr < self.height and 0 <= nc < self.width: result.append(self[nr, nc])
        return result

    def row_fill(self, row, item):
        """Fills a specified row with item. Return None."""
        if not 0 <= row < self.height: raise ValueError("Row index out of range")
        for col in range(self.width): self[row, col] = item

    def col_fill(self, col, item):
        """Fills a specified col with item. Return None."""
        if not 0 <= col < self.width: raise ValueError("Col index out of range")
        for row in range(self.height): self[row, col] = item

    def sum(self):
        """Sum all cell values (if numeric)."""
        return sum(val for _, _, val in self if isinstance(val, (int, float)))

    def min(self):
        """Find minimum cell value."""
        return min(val for _, _, val in self if val != self.background)

    def max(self):
        """Find maximum cell value."""
        return max(val for _, _, val in self if val != self.background)

    def map(self, func):
        """Apply function to each cell. Function can take (r,c,val) or just (val)"""
        new_board = self.copy()
        for r, c, val in new_board:
            try: new_board[r, c] = func(r, c, val)
            except TypeError: new_board[r, c] = func(val)
        return new_board

    def reverse(self, item, item2):
        """Swap item and item2 throughout the board."""
        clue = {item: item2, item2: item}
        for r, c in self.steps: self[r, c] = clue.get(self[r, c], self[r, c])

    def checkerboard(self, item1, item2):
        """Fill board with checkerboard pattern."""
        for r, c in self.steps: self[r, c] = item1 if (r + c) % 2 == 0 else item2

    def move(self, row, col, row2, col2):
        """Move item from (row,col) to (row2,col2)."""
        if not (0 <= row < self.height and 0 <= col < self.width): raise ValueError("Source position out of bounds")
        if not (0 <= row2 < self.height and 0 <= col2 < self.width): raise ValueError("Target position out of bounds")
        item = self[row, col]
        self[row, col] = self.background
        self[row2, col2] = item

    def uscroll(self, length):
        """Move board cells up for a specific length. Cells out of bounds appear back on the bottom. Return self."""
        self.body = self.body[length%self.height:]+self.body[:length%self.height]
        return self

    def dscroll(self, length):
        """Move board cells down for a specific length. Cells out of bounds appear back on the top. Return self."""
        self.body = self.body[-(length%self.height):]+self.body[:-(length%self.height)]
        return self

    def lscroll(self, length):
        """Move board cells left for a specific length. Cells out of bounds appear back on the right. Return self."""
        for r in range(self.height): self.body[r] = self.body[r][length%self.height:]+self.body[r][:length%self.height]
        return self

    def rscroll(self, length):
        """Move board cells right for a specific length. Cells out of bounds appear back on the left. Return self."""
        for r in range(self.height): self.body[r] = self.body[r][-(length%self.height):]+self.body[r][:-(length%self.height)]
        return self

    def uscrolled(self, length):
        """Return the scrolled Board by using uscroll. Does not modify original Board."""
        return self.copy().uscroll(length)

    def dscrolled(self, length):
        """Return the scrolled Board by using dscroll. Does not modify the original Board."""
        return self.copy().dscroll(length)

    def lscrolled(self, length):
        """Return the scrolled Board by using lscroll. Does not modify the original Board."""
        return self.copy().lscroll(length)

    def rscrolled(self, length):
        """Return the scrolled Board by using rscroll. Does not modify the original Board."""
        return self.copy().rscroll(length)

    def is_symmetric(self, way="both"):
        """
        Determine if a board is symmetric in a specified way.
        Valid str values of argument 'way':
        'horizontal' -> checks horizontally
        'vertical' -> checks vertically
        'both' -> checks both
        """
        copyself = self.copy()
        clue = {
            'horizontal': [self.hflip],
            'vertical': [self.vflip],
            'both': [self.hflip, self.vflip]
        }
        way = way.lower().replace(" ", "")
        if way not in clue.keys(): raise ValueError("Given way of symmetry must be 'horizontal', 'vertical' or 'both'")
        for func in clue[way]:
            transformed = self.copy()
            transformed.func()
            if self.body != transformed.body:
                return False
        return True

    def floodfill(self, row, col, item):
        """Flood fill connected region starting at row, col"""
        if self[row, col] != item:
            for r, c in self.connected(row, col): self[r, c] = item

    def similarity(self, other):
        """Calculates the percentage similarity of two boards."""
        if self.width != other.width or self.height != other.height: raise ValueError("Boards must be same with and height")
        count = 0
        for r, c, item in self:
            if other[r, c] == item: count += 1
        return count / self.size * 100

    def difference(self, other):
        """Calculates the percentage difference of two boards."""
        return 100 - self.similarity(other)
