diff --git a/Exercise 10/chess/board.py b/Exercise 10/chess/board.py index 7f8b2a2..4b47189 100644 --- a/Exercise 10/chess/board.py +++ b/Exercise 10/chess/board.py @@ -1,8 +1,28 @@ from typing import Callable, Iterable, Union from os import system +from shutil import get_terminal_size as getTerminalSize from piece import Piece -from util import centerText, centerBlockText, boxCharsToBoldBoxCharsMap as boldBoxChars, determineMove + + +def centerText(text): + terminalWidth = getTerminalSize((60, 0))[0] # Column size 60 as fallback + return "\n".join(line.center(terminalWidth) for line in text.split('\n')) + + +def centerBlockText(text): + terminalWidth = getTerminalSize((60, 0))[0] # Column size 60 as fallback + textArray = text.split('\n') + offset = int((terminalWidth - len(textArray[0])) / 2) + return "\n".join(offset * ' ' + line for line in textArray) + + +def determineMove(key, x, y, maxmin) -> tuple: + if key in ['s', 'j'] and y != maxmin[1]: return (0, 1) + elif key in ['w', 'k'] and y != maxmin[0]: return (0, -1) + elif key in ['d', 'l'] and x != maxmin[1]: return (1, 0) + elif key in ['a', 'h'] and x != maxmin[0]: return (-1, 0) + else: return False class Board: @@ -69,6 +89,20 @@ class Board: def highlightBox(x, y): """Make box around a position bold""" + boldBoxChars = { + '─': '═', + '│': '║', + '┼': '╬', + '╰': '╚', + '╯': '╝', + '╭': '╔', + '╮': '╗', + '├': '╠', + '┴': '╩', + '┤': '╣', + '┬': '╦', + } + pointsToChange = \ [(x * 4 + 0, y * 2 + i) for i in range(3)] + \ [(x * 4 + 4, y * 2 + i) for i in range(3)] + \ @@ -94,33 +128,46 @@ class Board: return '\n'.join([''.join(line) for line in stringArray]) def selectPiece(self, player, x=0, y=0, centering=True) -> tuple: - """Lets the user select a piece from a graphic board""" + """Lets the user select a piece""" while True: system('clear') - playerString = '\n' + player.name + '\n\n' - menuString = self.draw({'highlightedBoxes': [(x, y)]}) + '\n' + playerString = '\n' + player.name + '\n' + checkString = f"\033[41m{'CHECK' if self.checkCheck(player.color) else ''}\033[0m" + '\n' + + hoveringPiece = self.getPieceAt(x, y) + pieceIsOwnColor = hoveringPiece != None and hoveringPiece.color == player.color + + menuString = self.draw({ + 'highlightedBoxes': [(x, y)], + 'highlightedContent': Piece.possibleMoves(x, y, self) if pieceIsOwnColor else [] + }) + '\n' inputString = f" W E\nA S D <- Enter : " if centering: playerString = centerText(playerString) + checkString = centerText(checkString) menuString = centerBlockText(menuString) inputString = centerBlockText(inputString) print(playerString) + print(checkString) print(menuString) try: key = input(inputString)[0] - except IndexError: + except IndexError: # Input was empty key = '' + try: if move := determineMove(key, x, y, (0, 7)): x += move[0] y += move[1] - elif key == 'e' and self.getPieceAt(x, y).color == player.color: + elif key == 'e' \ + and hoveringPiece.color == player.color \ + and Piece.possibleMoves(x, y, self) != []: return (x, y) - except AttributeError: + except AttributeError: # Chosen tile contains no piece pass def selectMove(self, player, x, y, legalMoves, centering=True) -> Union[tuple, bool]: @@ -128,7 +175,8 @@ class Board: while True: system('clear') - playerString = '\n' + player.name + '\n\n' + playerString = '\n' + player.name + '\n' + checkString = f"\033[41m{'CHECK' if self.checkCheck(player.color) else ''}\033[0m" + '\n' menuString = self.draw({ 'highlightedBoxes': [(x, y)], 'highlightedContent': legalMoves @@ -137,16 +185,19 @@ class Board: if centering: playerString = centerText(playerString) + checkString = centerText(checkString) #TODO: Doesn't center because of escape chars menuString = centerBlockText(menuString) inputString = centerBlockText(inputString) print(playerString) + print(checkString) print(menuString) try: key = input(inputString)[0] - except IndexError: + except IndexError: # Input was empty key = '' + if move := determineMove(key, x, y, (0, 7)): x += move[0] y += move[1] @@ -155,14 +206,15 @@ class Board: elif key == 'e' and (x, y) in legalMoves: return (x, y) - def getPieceAt(self, x, y) -> Piece: + def getPieceAt(self, x, y) -> Union[Piece, None]: + """Gets a piece at a certain position""" try: return self.boardArray[y][x] - except IndexError: + except IndexError: # Outside board return None def getPositionsWhere(self, condition: Callable[[Piece], bool]) -> Iterable[tuple]: - """ Returns a list of xy pairs of the pieces where a condition is met """ + """Returns a list of xy pairs of the pieces where a condition is met """ result = [] for y, row in enumerate(self.boardArray): @@ -178,9 +230,9 @@ class Board: """Check whether a team is caught in check. The color is the color of the team to check""" king = self.getPositionsWhere(lambda piece: piece.type == 'k' and piece.color == color)[0] piecesToCheck = self.getPositionsWhere(lambda piece: piece.color != color) - return any([ - king in Piece.possibleMoves(*piece, self, simulation=simulation) for piece in piecesToCheck - ]) + # Resend simulation status into possibleMoves in order to avoid indefinite recursion + return any( + king in Piece.possibleMoves(*piece, self, simulation=simulation) for piece in piecesToCheck) def getPositionsToProtectKing(self, color) -> Iterable[tuple]: """Get a list of the positions to protect in order to protect the king when in check. The color is the color of the team who's in check""" @@ -196,18 +248,16 @@ class Board: result.append([piece]) if self.getPieceAt(*piece).type not in ['p', 'n', 'k']: - # Get the direction as a tuple from the threatening piece to the king def getDirection(fromPosition, toPosition) -> tuple: + """Get the direction as a tuple from the threatening piece to the king""" x = -1 if toPosition[0] > fromPosition[0] else \ 0 if toPosition[0] == fromPosition[0] else 1 y = -1 if toPosition[1] > fromPosition[1] else \ 0 if toPosition[1] == fromPosition[1] else 1 return (x, y) - direction = getDirection(piece, king) - - # Return a list of every position until the king def getPositionsUntilKing(x, y, direction) -> Iterable[tuple]: + """Return a list of every position until the king""" result = [] x += direction[0] y += direction[1] @@ -217,10 +267,11 @@ class Board: y += direction[1] return result + direction = getDirection(piece, king) result[-1] += getPositionsUntilKing(*king, direction) - # Combine lists so that only tuples in all the lists of threatening pieces are valid def getCommonValues(lst: Iterable[Iterable[tuple]]): + """Combine lists so that only tuples in all the lists of threatening pieces are valid""" result = set(lst[0]) for sublst in lst[1:]: result.intersection_update(sublst) @@ -229,6 +280,7 @@ class Board: return getCommonValues(result) def playerHasLegalMoves(self, color) -> bool: + """ returns whether or not a player has any legal moves left""" enemyPieces = self.getPositionsWhere(lambda piece: piece.color == color) if self.checkCheck(color): getLegalMoves = lambda piece: Piece.possibleMoves( @@ -236,7 +288,7 @@ class Board: else: getLegalMoves = lambda piece: Piece.possibleMoves(*piece, self) - return not any(getLegalMoves(piece) == None for piece in enemyPieces) + return any(getLegalMoves(piece) != [] for piece in enemyPieces) def checkStaleMate(self, color) -> bool: """Check whether a team is caught in stalemate. The color is the color of the team to check""" @@ -246,12 +298,12 @@ class Board: """Check whether a team is caught in checkmate. The color is the color of the team to check""" return self.checkCheck(color) and not self.playerHasLegalMoves(color) - def movePiece(self, position, toPosition, piecesToRemove=None): + def movePiece(self, position, toPosition, piecesToRemove=[]): + """ Move a piece from position to toPosition. In case of extra pieces to be removes, add them to the list piecesToRemove""" x, y = position toX, toY = toPosition self.boardArray[toY][toX] = self.boardArray[y][x] self.boardArray[y][x] = None - if piecesToRemove != None: - for x, y in piecesToRemove: - self.boardArray[y][x] = None + for x, y in piecesToRemove: + self.boardArray[y][x] = None diff --git a/Exercise 10/chess/chess.py b/Exercise 10/chess/chess.py index 0cdefda..6506103 100755 --- a/Exercise 10/chess/chess.py +++ b/Exercise 10/chess/chess.py @@ -32,6 +32,7 @@ class Chess: ░█▀▄░█░░░█▀█░█░░░█▀▄░░░█▄█░░█░░█░█ ░▀▀░░▀▀▀░▀░▀░▀▀▀░▀░▀░░░▀░▀░▀▀▀░▀░▀ ''') + input('Press any button to exit...') exit(0) def tie(self): @@ -40,10 +41,26 @@ class Chess: ░▀▀█░░█░░█▀█░█░░░█▀▀░█░█░█▀█░░█░░█▀▀ ░▀▀▀░░▀░░▀░▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░▀░░▀▀▀ ''') + input('Press any button to exit...') exit(0) + def promoteIfPossible(self, player, position): + promoteY = 0 if player.color == 'white' else 7 + if (piece := self.board.getPieceAt(*position)).type == 'p' and position[1] == promoteY: + while True: + answer = input('What would you like your pawn to become? (q,b,r or n) ') + if answer in 'qbrn' and len(answer) == 1: + break + else: + print('\nCouldn\'t parse input. Try again') + + piece.type = answer + + def makeMove(self, player): - chosenTile = 0, 0 + # Get the first piece belonging to the player + currentPlayersPiece = lambda piece: piece.color == player.color + chosenTile = self.board.getPositionsWhere(currentPlayersPiece)[0] while True: piece = self.board.selectPiece(player, *chosenTile) chosenTile = piece @@ -51,6 +68,7 @@ class Chess: if move := self.board.selectMove(player, *piece, possibleMoves): break self.board.movePiece(piece, move) + self.promoteIfPossible(player, move) def turn(self, playerNum): system('clear') diff --git a/Exercise 10/chess/piece.py b/Exercise 10/chess/piece.py index 5cc5dd1..62ede06 100644 --- a/Exercise 10/chess/piece.py +++ b/Exercise 10/chess/piece.py @@ -12,6 +12,7 @@ class Piece: def __str__(self): return self.type.upper() if self.color == 'white' else self.type + # Unused code. I'm missing the font for my terminal, but go ahead and use piece.symbol instead of str(symbol) in board.draw if you'd like @property def symbol(self): symbols = [{ @@ -54,8 +55,8 @@ class Piece: pieceIsEmptyOrEnemyColor = lambda pieceToCheck: pieceToCheck == None or pieceToCheck.color != piece.color positionInsideBounds = lambda x, y: x in range(8) and y in range(8) - def addMoveIfTrue(xOffset, yOffset, condition: Callable[[Piece], bool]): - """Tests a condition against a position away from self. Adds if condition returns true""" + def addMoveIfTrue(xOffset, yOffset, condition: Callable[[Piece], bool]) -> bool: + """Tests a condition against a position away from self. Adds move if condition returns true. Returns condition result""" if condition(board.getPieceAt(x + xOffset, y + yOffset)): moves.append((x + xOffset, y + yOffset)) return True @@ -73,21 +74,23 @@ class Piece: while positionInsideBounds(localX, localY): localX += direction[0] localY += direction[1] - if board.getPieceAt(localX, localY) == None: + currentPiece = board.getPieceAt(localX, localY) + if pieceIsEmpty(currentPiece): moves.append((localX, localY)) else: - if board.getPieceAt(localX, localY).color != piece.color: + if pieceIsEnemyColor(currentPiece): moves.append((localX, localY)) return if piece.type == 'p': localY = 1 if piece.color == 'black' else -1 startPosition = 1 if piece.color == 'black' else 6 + pieceAtStartPosition = lambda pieceToCheck: pieceToCheck == None and y == startPosition + addMoveIfTrue(1, localY, pieceIsEnemyColor) addMoveIfTrue(-1, localY, pieceIsEnemyColor) if addMoveIfTrue(0, localY, pieceIsEmpty): - addMoveIfTrue(0, localY * 2, - lambda pieceToCheck: pieceToCheck == None and y == startPosition) + addMoveIfTrue(0, localY * 2, pieceAtStartPosition) elif piece.type == 'n': positions = [ @@ -126,7 +129,7 @@ class Piece: # Remove moves that will lead the piece out of the board moves = [move for move in moves if positionInsideBounds(*move)] - # Remove moves that won't block the path for whichever piece has caused the game to check + # Remove moves that is not included in the legal moves (moves to block check) if legalMoves != None and piece.type != 'k': moves = [move for move in moves if move in legalMoves] diff --git a/Exercise 10/chess/util.py b/Exercise 10/chess/util.py deleted file mode 100644 index 8f497e2..0000000 --- a/Exercise 10/chess/util.py +++ /dev/null @@ -1,36 +0,0 @@ -from shutil import get_terminal_size as getTerminalSize - -# ░█▀▄░█▀█░█▀█░█▀▄░█▀█░█▄█░░░█▀▀░█░█░▀█▀░▀█▀ -# ░█▀▄░█▀█░█░█░█░█░█░█░█░█░░░▀▀█░█▀█░░█░░░█░ -# ░▀░▀░▀░▀░▀░▀░▀▀░░▀▀▀░▀░▀░░░▀▀▀░▀░▀░▀▀▀░░▀░ - -def centerText(text): - terminalWidth = getTerminalSize((60, 0))[0] # Column size 60 as fallback - return "\n".join(line.center(terminalWidth) for line in text.split('\n')) - -def centerBlockText(text): - terminalWidth = getTerminalSize((60, 0))[0] # Column size 60 as fallback - textArray = text.split('\n') - offset = int((terminalWidth - len(textArray[0])) / 2) - return "\n".join(offset * ' ' + line for line in textArray) - -boxCharsToBoldBoxCharsMap = { - '─': '═', - '│': '║', - '┼': '╬', - '╰': '╚', - '╯': '╝', - '╭': '╔', - '╮': '╗', - '├': '╠', - '┴': '╩', - '┤': '╣', - '┬': '╦', -} - -def determineMove(key, x, y, maxmin) -> tuple: - if key in ['s', 'j'] and y != maxmin[1]: return (0, 1) - elif key in ['w', 'k'] and y != maxmin[0]: return (0, -1) - elif key in ['d', 'l'] and x != maxmin[1]: return (1, 0) - elif key in ['a', 'h'] and x != maxmin[0]: return (-1, 0) - else: return False