From 23adf993472b6af31910bed5f08aeafac8f2625b Mon Sep 17 00:00:00 2001 From: h7x4 Date: Thu, 12 Nov 2020 19:17:16 +0100 Subject: [PATCH] Make pieces move --- Exercise 10/chess/board.py | 153 +++++++++++++++++++++---------------- Exercise 10/chess/chess.py | 46 ++++++++--- Exercise 10/chess/piece.py | 65 ++++++++++++---- Exercise 10/chess/util.py | 36 +++++++++ 4 files changed, 210 insertions(+), 90 deletions(-) create mode 100644 Exercise 10/chess/util.py diff --git a/Exercise 10/chess/board.py b/Exercise 10/chess/board.py index a6163f2..87f1bce 100644 --- a/Exercise 10/chess/board.py +++ b/Exercise 10/chess/board.py @@ -1,20 +1,20 @@ -from typing import Callable, Iterable +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, determineMove class Board: - def __init__(self): + def __init__(self, boardState=None): self.boardArray = [ [Piece(type, 'black') for type in ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']], [Piece('p', 'black') for _ in range(8)], *[[None for _ in range(8)] for _ in range(4)], [Piece('p', 'white') for _ in range(8)], [Piece(type, 'white') for type in ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']], - ] + ] if boardState == None else boardState def draw( self, @@ -43,7 +43,7 @@ class Board: # Draw general outline stringArray = [list('┼' + '───┼' * 8)] + [[None] for _ in range(8 * 2)] for y, row in enumerate(self.boardArray): - for x, column in enumerate(row): + for x, _ in enumerate(row): stringArray[2 * y + 1][4 * x] = '│' stringArray[2 * y + 2][4 * x] = '┼' @@ -65,30 +65,15 @@ class Board: stringArray[i * 2 + 2][0] = '├' stringArray[i * 2 + 2][-1] = '┤' - def highlightContent(x, y): + def highlightContent(x, y, escapeCodes=config['highlightEscapeCodes']): """highlight contents of a piece with xterm-256colors modifiers""" - stringArray[y * 2 + - 1][x * 4 + - 1] = config['highlightEscapeCodes'][0] + stringArray[y * 2 + 1][x * 4 + 1] - stringArray[y * 2 + 1][x * 4 + 3] += config['highlightEscapeCodes'][1] + 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] def highlightBox(x, y): """Make box around a piece bold""" - characterMap = { - '─': '═', - '│': '║', - '┼': '╬', - '╰': '╚', - '╯': '╝', - '╭': '╔', - '╮': '╗', - '├': '╠', - '┴': '╩', - '┤': '╣', - '┬': '╦', - } - pointsToChange = \ [(x * 4 + 0, y * 2 + i) for i in range(3)] + \ [(x * 4 + 4, y * 2 + i) for i in range(3)] + \ @@ -96,13 +81,18 @@ class Board: [(x * 4 + i, y * 2 + 2) for i in range(1,4)] for x, y in pointsToChange: - stringArray[y][x] = characterMap[ - stringArray[y][x]] if stringArray[y][x] in characterMap else stringArray[y][x] + stringArray[y][x] = boxCharsToBoldBoxCharsMap[ + stringArray[y] + [x]] if stringArray[y][x] in boxCharsToBoldBoxCharsMap else stringArray[y][x] - for x, y in config['highlightedBoxes']: - highlightBox(x, y) - for x, y in config['highlightedContent']: - highlightContent(x, y) + # 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) return '\n'.join([''.join(line) for line in stringArray]) @@ -111,35 +101,68 @@ class Board: while True: system('clear') - menuString = '\n' + player.name + '\n\n' - menuString += self.draw({'highlightedBoxes': [(x, y)]}) + '\n' + playerString = '\n' + player.name + '\n\n' + menuString = self.draw({'highlightedBoxes': [(x, y)]}) + '\n' inputString = f" W E\nA S D <- Enter : " - 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) - if centering: - menuString = centerText(menuString) + playerString = centerText(playerString) + menuString = centerBlockText(menuString) inputString = centerBlockText(inputString) + print(playerString) print(menuString) - key = input(inputString)[0] - if key in ['s', 'j'] and y != 7: y += 1 - elif key in ['w', 'k'] and y != 0: y -= 1 - elif key in ['d', 'l'] and x != 7: x += 1 - elif key in ['a', 'h'] and x != 0: x -= 1 - elif key == 'e': return (x, y) + try: + key = input(inputString)[0] + except IndexError: + 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: + return (x, y) + except AttributeError: + pass + + def selectMove(self, player, x, y, legalMoves, centering=True) -> Union[tuple, bool]: + """Lets the user select a move to make from a graphic board""" + + while True: + system('clear') + playerString = '\n' + player.name + '\n\n' + menuString = self.draw({ + 'highlightedBoxes': [(x, y)], + 'highlightedContent': legalMoves + }) + '\n' + inputString = f"Q W E\nA S D <- Enter : " + + if centering: + playerString = centerText(playerString) + menuString = centerBlockText(menuString) + inputString = centerBlockText(inputString) + + print(playerString) + print(menuString) + + try: + key = input(inputString)[0] + except IndexError: + key = '' + if move := determineMove(key, x, y, (0, 7)): + x += move[0] + y += move[1] + elif key == 'q': + return False + elif key == 'e' and (x, y) in legalMoves: + return (x, y) def getPieceAt(self, x, y) -> Piece: - return self.boardArray[y][x] + try: + return self.boardArray[y][x] + except IndexError: + 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 """ @@ -147,19 +170,22 @@ class Board: result = [] for y, row in enumerate(self.boardArray): for x, piece in enumerate(row): - if condition(piece): - result.append((x, y)) + try: + if condition(piece): + result.append((x, y)) + except AttributeError: + pass return result def checkCheck(self, color) -> 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) + 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]) 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) + 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): @@ -178,7 +204,7 @@ class Board: direction = getDirection(piece, king) - def getPositionsUntilKing(x, y, direction): + def getPositionsUntilKing(x, y, direction) -> Iterable[tuple]: result = [] while self.getPieceAt(x, y) == None: result.append((x, y)) @@ -190,19 +216,19 @@ class Board: return 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)) + return not any(getLegalMoves(piece) == None 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""" - enemyPieces = self.getPositionsWhere(lambda piece: piece.color == color) - getLegalMoves = lambda piece: Piece.possibleMoves(*piece, self, legalMoves = self.getPositionsToProtectKing(color)) - piecesHasNoLegalMoves = any( getLegalMoves(piece) == None for piece in enemyPieces) - return (not self.checkCheck(color)) and piecesHasNoLegalMoves + return (not self.checkCheck(color)) and not self.playerHasLegalMoves(color) def checkCheckMate(self, color) -> bool: """Check whether a team is caught in checkmate. The color is the color of the team to check""" - enemyPieces = self.getPositionsWhere(lambda piece: piece.color == color) - getLegalMoves = lambda piece: Piece.possibleMoves(*piece, self, legalMoves = self.getPositionsToProtectKing(color)) - piecesHasNoLegalMoves = any( getLegalMoves(piece) == None for piece in enemyPieces) - return self.checkCheck(color) and piecesHasNoLegalMoves + return self.checkCheck(color) and not self.playerHasLegalMoves(color) def movePiece(self, position, toPosition, piecesToRemove=None): x, y = position @@ -213,4 +239,3 @@ class Board: if piecesToRemove != 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 431c2cc..e6e58dd 100644 --- a/Exercise 10/chess/chess.py +++ b/Exercise 10/chess/chess.py @@ -2,6 +2,7 @@ from os import system from dataclasses import dataclass from board import Board +from piece import Piece @dataclass class Player: @@ -12,21 +13,42 @@ class Chess: def __init__(self, players): self.players = players + self.board = Board() - def lose(self, player): - print(player.name, 'lost.') + def makeMove(self, player): + 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): system('clear') - board.selectPiece(players[0]) - if board.checkCheckMate(players[1].color): - self.lose(players[1]) + 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') - board.selectPiece(players[1]) - if board.checkCheckMate(players[0].color): - self.lose(players[0]) + self.makeMove(players[1]) + # if self.board.checkCheckMate(players[0].color): + # self.win(players[1]) + # elif self.board.checkStaleMate(players[0].color): + # self.tie() def loop(self): while True: @@ -40,7 +62,7 @@ if __name__ == "__main__": Player('Spiller 2', 'black'), ) - # game = Chess(('Spiller 1', 'Spiller 2')) - # game.loop() - board = Board() - print(board.selectPiece(players[0], centering=True)) + 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 b21c558..dd774b8 100644 --- a/Exercise 10/chess/piece.py +++ b/Exercise 10/chess/piece.py @@ -1,6 +1,8 @@ from typing import Iterable, Callable from itertools import product from copy import deepcopy +from sys import setrecursionlimit as setRecursionLimit +setRecursionLimit(100000) class Piece: @@ -12,6 +14,25 @@ class Piece: def __str__(self): return self.type.upper() if self.color == 'white' else self.type + @property + def symbol(self): + symbols = [{ + 'p': '♙', + 'r': '♖', + 'n': '♘', + 'b': '♗', + 'q': '♕', + 'k': '♔', + }, { + 'p': '♟︎', + 'r': '♜', + 'n': '♞', + 'b': '♝', + 'q': '♛', + 'k': '♚', + }] + return symbols[0 if self.color == 'white' else 1][self.type] + @staticmethod def possibleMoves(x, y, board, legalMoves=None) -> Callable[[int, int], Iterable[tuple]]: piece = board.getPieceAt(x, y) @@ -22,6 +43,7 @@ class Piece: 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 + 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""" @@ -30,15 +52,15 @@ class Piece: return True return False + # TODO: fix deepcopy segfault and recursion error def assertNotCheck(newX, newY) -> bool: testBoard = deepcopy(board) - testBoard.movePiece((x, y), (newX, newY)) # J - return not testBoard.checkCheck() + testBoard.movePiece((x, y), (newX, newY)) + return not testBoard.checkCheck(piece.color) def addWhileInsideBoard(direction: tuple): - localX = x - localY = y - while localX not in [0, 7] and localY not in [0, 7]: + localX, localY = x, y + while positionInsideBounds(localX, localY): localX += direction[0] localY += direction[1] if board.getPieceAt(localX, localY) == None: @@ -49,13 +71,25 @@ class Piece: return if piece.type == 'p': - addMoveIfTrue(1, 1, pieceIsEnemyColor) - addMoveIfTrue(-1, 1, pieceIsEnemyColor) - if addMoveIfTrue(0, 1, pieceIsEmpty): - addMoveIfTrue(0, 2, lambda pieceToCheck: pieceToCheck == None and piece.moves == 0) + localY = 1 if piece.color == 'black' else -1 + startPosition = 1 if piece.color == 'black' else 6 + addMoveIfTrue(1, localY, pieceIsEnemyColor) + addMoveIfTrue(-1, localY, pieceIsEnemyColor) + if addMoveIfTrue(0, localY, pieceIsEmpty): + addMoveIfTrue(0, localY * 2, + lambda pieceToCheck: pieceToCheck == None and y == startPosition) elif piece.type == 'n': - positions = [(-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2, 1)] + positions = [ + (-2, -1), + (-2, 1), + (-1, -2), + (-1, 2), + (1, -2), + (1, 2), + (2, -1), + (2, 1), + ] for position in positions: addMoveIfTrue(*position, pieceIsEmptyOrEnemyColor) @@ -65,10 +99,8 @@ class Piece: for position in positions: addMoveIfTrue(*position, pieceIsEmptyOrEnemyColor) - moves = [position for position in moves if assertNotCheck(*position)] - elif piece.type == 'r': - for direction in product([0, 1], repeat=2): + for direction in [(1, 0), (-1, 0), (0, 1), (0, -1)]: addWhileInsideBoard(direction) elif piece.type == 'b': @@ -81,9 +113,14 @@ class Piece: for direction in directions: addWhileInsideBoard(direction) - #TODO: remove moves that will put the king in check + # 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 if legalMoves != None and piece.type != 'k': 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)] + return moves diff --git a/Exercise 10/chess/util.py b/Exercise 10/chess/util.py new file mode 100644 index 0000000..8f497e2 --- /dev/null +++ b/Exercise 10/chess/util.py @@ -0,0 +1,36 @@ +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