diff --git a/Exercise 10/chess/board.py b/Exercise 10/chess/board.py index 87f1bce..7f8b2a2 100644 --- a/Exercise 10/chess/board.py +++ b/Exercise 10/chess/board.py @@ -2,12 +2,13 @@ from typing import Callable, Iterable, Union from os import system from piece import Piece -from util import centerText, centerBlockText, boxCharsToBoldBoxCharsMap, determineMove +from util import centerText, centerBlockText, boxCharsToBoldBoxCharsMap as boldBoxChars, determineMove class Board: def __init__(self, boardState=None): + """Create a standard board if nothing else is defined in boardState""" self.boardArray = [ [Piece(type, 'black') for type in ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']], [Piece('p', 'black') for _ in range(8)], @@ -16,14 +17,7 @@ class Board: [Piece(type, 'white') for type in ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']], ] if boardState == None else boardState - def draw( - self, - config={ - 'highlightedContent': [], - 'highlightEscapeCodes': ('\033[32;5;7m', '\033[0m'), - 'highlightedBoxes': [], - } - ) -> str: + def draw(self, config={}) -> str: """Returns a string representing the board config options: @@ -32,6 +26,7 @@ class Board: highlightedBoxes: [(x,y)] - Boxes to make bold """ + # Fill default values in config dict def fillConfigDefaultValue(key, defaultValue): if key not in config: config[key] = defaultValue @@ -40,15 +35,15 @@ class Board: fillConfigDefaultValue('highlightedBoxes', []) fillConfigDefaultValue('highlightEscapeCodes', ('\033[32;5;7m', '\033[0m')) - # Draw general outline + # Draw general outline with ┼ as all corners stringArray = [list('┼' + '───┼' * 8)] + [[None] for _ in range(8 * 2)] for y, row in enumerate(self.boardArray): for x, _ in enumerate(row): stringArray[2 * y + 1][4 * x] = '│' stringArray[2 * y + 2][4 * x] = '┼' - stringArray[2 * y + 1] += list( - ' {} │'.format(str(self.boardArray[y][x]) if self.boardArray[y][x] != None else ' ')) + symbol = str(self.boardArray[y][x]) if self.boardArray[y][x] != None else ' ' + stringArray[2 * y + 1] += list(' {} │'.format(symbol)) stringArray[2 * y + 2] += list('───┼') # Overwrite corners @@ -58,21 +53,21 @@ class Board: stringArray[-1][-1] = '╯' # Overwrite T-junctions - for i in range(int(len(stringArray[0]) / 4) - 1): + for i in range(int(len(stringArray[0]) / 4) - 1): # Loop row stringArray[0][i * 4 + 4] = '┬' stringArray[-1][i * 4 + 4] = '┴' - for i in range(int(len(stringArray) / 2) - 1): + for i in range(int(len(stringArray) / 2) - 1): # Loop column stringArray[i * 2 + 2][0] = '├' stringArray[i * 2 + 2][-1] = '┤' - def highlightContent(x, y, escapeCodes=config['highlightEscapeCodes']): - """highlight contents of a piece with xterm-256colors modifiers""" + def highlightContent(x, y, modifiers=config['highlightEscapeCodes']): + """highlight inner part of a box with xterm-256colors modifiers""" stringArray[y * 2 + 1][x * 4 + 1] = \ - escapeCodes[0] + stringArray[y * 2 + 1][x * 4 + 1] - stringArray[y * 2 + 1][x * 4 + 3] += escapeCodes[1] + modifiers[0] + stringArray[y * 2 + 1][x * 4 + 1] + stringArray[y * 2 + 1][x * 4 + 3] += modifiers[1] def highlightBox(x, y): - """Make box around a piece bold""" + """Make box around a position bold""" pointsToChange = \ [(x * 4 + 0, y * 2 + i) for i in range(3)] + \ @@ -80,17 +75,19 @@ class Board: [(x * 4 + i, y * 2 + 0) for i in range(1,4)] + \ [(x * 4 + i, y * 2 + 2) for i in range(1,4)] + # This has to doublecheck that the character exists, because if neighbour + # boxes are to be highlighed, it will try to overwrite already bold borders for x, y in pointsToChange: - stringArray[y][x] = boxCharsToBoldBoxCharsMap[ - stringArray[y] - [x]] if stringArray[y][x] in boxCharsToBoldBoxCharsMap else stringArray[y][x] + symbolExists = stringArray[y][x] in boldBoxChars + stringArray[y][x] = boldBoxChars[stringArray[y][x]] if symbolExists else stringArray[y][x] - # Color white pieces - for piece in self.getPositionsWhere(lambda piece: piece.color == 'white'): - highlightContent(*piece, ('\033[7m', '\033[0m')) + # Color white pieces + for piece in self.getPositionsWhere(lambda piece: piece.color == 'white'): + highlightContent(*piece, ('\033[7m', '\033[0m')) for box in config['highlightedBoxes']: highlightBox(*box) + for piece in config['highlightedContent']: highlightContent(*piece) @@ -121,7 +118,7 @@ class Board: 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 self.getPieceAt(x, y).color == player.color: return (x, y) except AttributeError: pass @@ -173,28 +170,33 @@ class Board: try: if condition(piece): result.append((x, y)) - except AttributeError: + except AttributeError: # Position is None pass return result - def checkCheck(self, color) -> bool: + def checkCheck(self, color, simulation=False) -> bool: """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) for piece in piecesToCheck]) + 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""" king = self.getPositionsWhere(lambda piece: piece.type == 'k' and piece.color == color)[0] piecesToCheck = self.getPositionsWhere(lambda piece: piece.color != color) - for piece in piecesToCheck: - if king not in Piece.possibleMoves(*piece, self): - piecesToCheck.remove(piece) + + # Get all pieces that threaten the king + piecesToCheck = [piece for piece in piecesToCheck if king in Piece.possibleMoves(*piece, self)] + + # Add only self if piece is pawn, knight or king result = [] for piece in piecesToCheck: - result.append(piece) + 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: x = -1 if toPosition[0] > fromPosition[0] else \ 0 if toPosition[0] == fromPosition[0] else 1 @@ -204,22 +206,36 @@ class Board: direction = getDirection(piece, king) + # Return a list of every position until the king def getPositionsUntilKing(x, y, direction) -> Iterable[tuple]: result = [] + x += direction[0] + y += direction[1] while self.getPieceAt(x, y) == None: result.append((x, y)) x += direction[0] y += direction[1] return result - result += getPositionsUntilKing(*piece, direction) + result[-1] += getPositionsUntilKing(*king, direction) - return result + # Combine lists so that only tuples in all the lists of threatening pieces are valid + def getCommonValues(lst: Iterable[Iterable[tuple]]): + result = set(lst[0]) + for sublst in lst[1:]: + result.intersection_update(sublst) + return result + + return getCommonValues(result) def playerHasLegalMoves(self, color) -> bool: enemyPieces = self.getPositionsWhere(lambda piece: piece.color == color) - getLegalMoves = lambda piece: Piece.possibleMoves( - *piece, self, legalMoves=self.getPositionsToProtectKing(color)) + if self.checkCheck(color): + getLegalMoves = lambda piece: Piece.possibleMoves( + *piece, self, legalMoves=self.getPositionsToProtectKing(color)) + else: + getLegalMoves = lambda piece: Piece.possibleMoves(*piece, self) + return not any(getLegalMoves(piece) == None for piece in enemyPieces) def checkStaleMate(self, color) -> bool: diff --git a/Exercise 10/chess/chess.py b/Exercise 10/chess/chess.py old mode 100644 new mode 100755 index e6e58dd..0cdefda --- a/Exercise 10/chess/chess.py +++ b/Exercise 10/chess/chess.py @@ -1,58 +1,70 @@ +#!/bin/python3 + from os import system from dataclasses import dataclass from board import Board from piece import Piece + @dataclass class Player: name: str color: str + class Chess: def __init__(self, players): self.players = players self.board = Board() + def win(self, player): + if player.color == 'white': + print(''' +░█░█░█░█░▀█▀░▀█▀░█▀▀░░░█░█░▀█▀░█▀█ +░█▄█░█▀█░░█░░░█░░█▀▀░░░█▄█░░█░░█░█ +░▀░▀░▀░▀░▀▀▀░░▀░░▀▀▀░░░▀░▀░▀▀▀░▀░▀ + ''') + else: + print(''' +░█▀▄░█░░░█▀█░█▀▀░█░█░░░█░█░▀█▀░█▀█ +░█▀▄░█░░░█▀█░█░░░█▀▄░░░█▄█░░█░░█░█ +░▀▀░░▀▀▀░▀░▀░▀▀▀░▀░▀░░░▀░▀░▀▀▀░▀░▀ + ''') + exit(0) + + def tie(self): + print(''' +░█▀▀░▀█▀░█▀█░█░░░█▀▀░█▄█░█▀█░▀█▀░█▀▀ +░▀▀█░░█░░█▀█░█░░░█▀▀░█░█░█▀█░░█░░█▀▀ +░▀▀▀░░▀░░▀░▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░▀░░▀▀▀ + ''') + exit(0) + def makeMove(self, player): - chosenTile = 0,0 + chosenTile = 0, 0 while True: piece = self.board.selectPiece(player, *chosenTile) chosenTile = piece possibleMoves = Piece.possibleMoves(*piece, self.board) if move := self.board.selectMove(player, *piece, possibleMoves): break - print(move) - input() self.board.movePiece(piece, move) - def win(self, player): - print(player.name, 'won.') - exit(0) - - def tie(self): - print('Stalemate') - exit(0) - - def update(self): + def turn(self, playerNum): system('clear') - self.makeMove(players[0]) - # if self.board.checkCheckMate(players[1].color): - # self.win(players[0]) - # elif self.board.checkStaleMate(players[1].color): - # self.tie() - - system('clear') - self.makeMove(players[1]) - # if self.board.checkCheckMate(players[0].color): - # self.win(players[1]) - # elif self.board.checkStaleMate(players[0].color): - # self.tie() + self.makeMove(players[playerNum]) + # 1 - 1 = 0 and 1 - 0 = 1 + if self.board.checkCheckMate(players[1 - playerNum].color): + self.win(players[playerNum]) + elif self.board.checkStaleMate(players[1 - playerNum].color): + self.tie() def loop(self): while True: - self.update() + self.turn(0) + self.turn(1) if __name__ == "__main__": @@ -64,5 +76,3 @@ if __name__ == "__main__": game = Chess(('Spiller 1', 'Spiller 2')) game.loop() - # board = Board() - # print(board.selectPiece(players[0], centering=True)) diff --git a/Exercise 10/chess/piece.py b/Exercise 10/chess/piece.py index dd774b8..5cc5dd1 100644 --- a/Exercise 10/chess/piece.py +++ b/Exercise 10/chess/piece.py @@ -1,8 +1,6 @@ from typing import Iterable, Callable from itertools import product from copy import deepcopy -from sys import setrecursionlimit as setRecursionLimit -setRecursionLimit(100000) class Piece: @@ -34,12 +32,23 @@ class Piece: return symbols[0 if self.color == 'white' else 1][self.type] @staticmethod - def possibleMoves(x, y, board, legalMoves=None) -> Callable[[int, int], Iterable[tuple]]: + def possibleMoves( + x, + y, + board, + legalMoves=None, + simulation=False, + ) -> Callable[[int, int], Iterable[tuple]]: + """ + Calculate all possible moves for a piece at (x, y) given a board in a certain state. + + If there is restrictions for where the piece can go, the legal moves can be set to these. + If the function is part of a simulation, simulation needs to be set to True so that it doesn't keep on recursing simulation indefinetely. + """ + piece = board.getPieceAt(x, y) moves = [] - # TODO: If player is in check, only validate the moves that are leading to places that can be blocked in order to save the king. Make a function that overwrites `moves`. Notice: The king is the only piece that can 'Escape' to a position that may or may not be in the list of positions to be blocked. Make an exception - pieceIsEnemyColor = lambda pieceToCheck: pieceToCheck != None and pieceToCheck.color != piece.color pieceIsEmpty = lambda pieceToCheck: pieceToCheck == None pieceIsEmptyOrEnemyColor = lambda pieceToCheck: pieceToCheck == None or pieceToCheck.color != piece.color @@ -52,13 +61,14 @@ class Piece: return True return False - # TODO: fix deepcopy segfault and recursion error def assertNotCheck(newX, newY) -> bool: + """Simulate a move and return whether or not the move will result in check""" testBoard = deepcopy(board) testBoard.movePiece((x, y), (newX, newY)) - return not testBoard.checkCheck(piece.color) + return not testBoard.checkCheck(piece.color, simulation=True) def addWhileInsideBoard(direction: tuple): + """Adds moves in direction until it either hits a piece or the edge""" localX, localY = x, y while positionInsideBounds(localX, localY): localX += direction[0] @@ -121,6 +131,7 @@ class Piece: moves = [move for move in moves if move in legalMoves] # Remove moves that will put the king in check - # moves = [position for position in moves if assertNotCheck(*position)] + if not simulation: + moves = [position for position in moves if assertNotCheck(*position)] return moves