TDT4109/Exercise 10/chess/board.py

310 lines
11 KiB
Python
Raw Permalink Normal View History

2020-11-12 19:17:16 +01:00
from typing import Callable, Iterable, Union
2020-11-10 23:56:21 +01:00
from os import system
2020-11-13 18:48:48 +01:00
from shutil import get_terminal_size as getTerminalSize
2020-11-10 23:56:21 +01:00
from piece import Piece
2020-11-13 18:48:48 +01:00
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
2020-11-10 23:56:21 +01:00
class Board:
2020-11-12 19:17:16 +01:00
def __init__(self, boardState=None):
2020-11-12 23:43:39 +01:00
"""Create a standard board if nothing else is defined in boardState"""
2020-11-10 23:56:21 +01:00
self.boardArray = [
2020-11-12 00:17:02 +01:00
[Piece(type, 'black') for type in ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']],
2020-11-10 23:56:21 +01:00
[Piece('p', 'black') for _ in range(8)],
*[[None for _ in range(8)] for _ in range(4)],
[Piece('p', 'white') for _ in range(8)],
2020-11-12 00:17:02 +01:00
[Piece(type, 'white') for type in ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']],
2020-11-12 19:17:16 +01:00
] if boardState == None else boardState
2020-11-10 23:56:21 +01:00
2020-11-12 23:43:39 +01:00
def draw(self, config={}) -> str:
2020-11-10 23:56:21 +01:00
"""Returns a string representing the board
config options:
highlightedContent: [(x,y)] - Pieces to color
highlightEscapeCodes: (str, str) - Terminal escape codes to color highlightedContent with
highlightedBoxes: [(x,y)] - Boxes to make bold
"""
2020-11-12 23:43:39 +01:00
# Fill default values in config dict
2020-11-10 23:56:21 +01:00
def fillConfigDefaultValue(key, defaultValue):
if key not in config:
config[key] = defaultValue
fillConfigDefaultValue('highlightedContent', [])
fillConfigDefaultValue('highlightedBoxes', [])
fillConfigDefaultValue('highlightEscapeCodes', ('\033[32;5;7m', '\033[0m'))
2020-11-12 23:43:39 +01:00
# Draw general outline with ┼ as all corners
2020-11-10 23:56:21 +01:00
stringArray = [list('' + '───┼' * 8)] + [[None] for _ in range(8 * 2)]
for y, row in enumerate(self.boardArray):
2020-11-12 19:17:16 +01:00
for x, _ in enumerate(row):
2020-11-10 23:56:21 +01:00
stringArray[2 * y + 1][4 * x] = ''
stringArray[2 * y + 2][4 * x] = ''
2020-11-12 23:43:39 +01:00
symbol = str(self.boardArray[y][x]) if self.boardArray[y][x] != None else ' '
stringArray[2 * y + 1] += list(' {}'.format(symbol))
2020-11-10 23:56:21 +01:00
stringArray[2 * y + 2] += list('───┼')
# Overwrite corners
stringArray[0][0] = ''
stringArray[0][-1] = ''
stringArray[-1][0] = ''
stringArray[-1][-1] = ''
# Overwrite T-junctions
2020-11-12 23:43:39 +01:00
for i in range(int(len(stringArray[0]) / 4) - 1): # Loop row
2020-11-10 23:56:21 +01:00
stringArray[0][i * 4 + 4] = ''
stringArray[-1][i * 4 + 4] = ''
2020-11-12 23:43:39 +01:00
for i in range(int(len(stringArray) / 2) - 1): # Loop column
2020-11-10 23:56:21 +01:00
stringArray[i * 2 + 2][0] = ''
stringArray[i * 2 + 2][-1] = ''
2020-11-12 23:43:39 +01:00
def highlightContent(x, y, modifiers=config['highlightEscapeCodes']):
"""highlight inner part of a box with xterm-256colors modifiers"""
2020-11-12 19:17:16 +01:00
stringArray[y * 2 + 1][x * 4 + 1] = \
2020-11-12 23:43:39 +01:00
modifiers[0] + stringArray[y * 2 + 1][x * 4 + 1]
stringArray[y * 2 + 1][x * 4 + 3] += modifiers[1]
2020-11-10 23:56:21 +01:00
def highlightBox(x, y):
2020-11-12 23:43:39 +01:00
"""Make box around a position bold"""
2020-11-10 23:56:21 +01:00
2020-11-13 18:48:48 +01:00
boldBoxChars = {
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
'': '',
}
2020-11-10 23:56:21 +01:00
pointsToChange = \
[(x * 4 + 0, y * 2 + i) for i in range(3)] + \
[(x * 4 + 4, y * 2 + i) for i in range(3)] + \
[(x * 4 + i, y * 2 + 0) for i in range(1,4)] + \
[(x * 4 + i, y * 2 + 2) for i in range(1,4)]
2020-11-12 23:43:39 +01:00
# This has to doublecheck that the character exists, because if neighbour
# boxes are to be highlighed, it will try to overwrite already bold borders
2020-11-10 23:56:21 +01:00
for x, y in pointsToChange:
2020-11-12 23:43:39 +01:00
symbolExists = stringArray[y][x] in boldBoxChars
stringArray[y][x] = boldBoxChars[stringArray[y][x]] if symbolExists else stringArray[y][x]
2020-11-12 19:17:16 +01:00
2020-11-12 23:43:39 +01:00
# Color white pieces
for piece in self.getPositionsWhere(lambda piece: piece.color == 'white'):
highlightContent(*piece, ('\033[7m', '\033[0m'))
2020-11-10 23:56:21 +01:00
2020-11-12 19:17:16 +01:00
for box in config['highlightedBoxes']:
highlightBox(*box)
2020-11-12 23:43:39 +01:00
2020-11-12 19:17:16 +01:00
for piece in config['highlightedContent']:
highlightContent(*piece)
2020-11-10 23:56:21 +01:00
return '\n'.join([''.join(line) for line in stringArray])
2020-11-12 00:17:02 +01:00
def selectPiece(self, player, x=0, y=0, centering=True) -> tuple:
2020-11-13 18:48:48 +01:00
"""Lets the user select a piece"""
2020-11-10 23:56:21 +01:00
while True:
system('clear')
2020-11-13 18:48:48 +01:00
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'
2020-11-12 00:17:02 +01:00
inputString = f" W E\nA S D <- Enter : "
2020-11-12 19:17:16 +01:00
if centering:
playerString = centerText(playerString)
2020-11-13 18:48:48 +01:00
checkString = centerText(checkString)
2020-11-12 19:17:16 +01:00
menuString = centerBlockText(menuString)
inputString = centerBlockText(inputString)
2020-11-12 00:17:02 +01:00
2020-11-12 19:17:16 +01:00
print(playerString)
2020-11-13 18:48:48 +01:00
print(checkString)
2020-11-12 19:17:16 +01:00
print(menuString)
try:
key = input(inputString)[0]
2020-11-13 18:48:48 +01:00
except IndexError: # Input was empty
2020-11-12 19:17:16 +01:00
key = ''
2020-11-13 18:48:48 +01:00
2020-11-12 19:17:16 +01:00
try:
if move := determineMove(key, x, y, (0, 7)):
x += move[0]
y += move[1]
2020-11-13 18:48:48 +01:00
elif key == 'e' \
and hoveringPiece.color == player.color \
and Piece.possibleMoves(x, y, self) != []:
2020-11-12 19:17:16 +01:00
return (x, y)
2020-11-13 18:48:48 +01:00
except AttributeError: # Chosen tile contains no piece
2020-11-12 19:17:16 +01:00
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')
2020-11-13 18:48:48 +01:00
playerString = '\n' + player.name + '\n'
checkString = f"\033[41m{'CHECK' if self.checkCheck(player.color) else ''}\033[0m" + '\n'
2020-11-12 19:17:16 +01:00
menuString = self.draw({
'highlightedBoxes': [(x, y)],
'highlightedContent': legalMoves
}) + '\n'
inputString = f"Q W E\nA S D <- Enter : "
2020-11-12 00:17:02 +01:00
if centering:
2020-11-12 19:17:16 +01:00
playerString = centerText(playerString)
2020-11-13 18:48:48 +01:00
checkString = centerText(checkString) #TODO: Doesn't center because of escape chars
2020-11-12 19:17:16 +01:00
menuString = centerBlockText(menuString)
2020-11-12 00:17:02 +01:00
inputString = centerBlockText(inputString)
2020-11-10 23:56:21 +01:00
2020-11-12 19:17:16 +01:00
print(playerString)
2020-11-13 18:48:48 +01:00
print(checkString)
2020-11-12 00:17:02 +01:00
print(menuString)
2020-11-12 19:17:16 +01:00
try:
key = input(inputString)[0]
2020-11-13 18:48:48 +01:00
except IndexError: # Input was empty
2020-11-12 19:17:16 +01:00
key = ''
2020-11-13 18:48:48 +01:00
2020-11-12 19:17:16 +01:00
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)
2020-11-10 23:56:21 +01:00
2020-11-13 18:48:48 +01:00
def getPieceAt(self, x, y) -> Union[Piece, None]:
"""Gets a piece at a certain position"""
2020-11-12 19:17:16 +01:00
try:
return self.boardArray[y][x]
2020-11-13 18:48:48 +01:00
except IndexError: # Outside board
2020-11-12 19:17:16 +01:00
return None
2020-11-12 00:17:02 +01:00
def getPositionsWhere(self, condition: Callable[[Piece], bool]) -> Iterable[tuple]:
2020-11-13 18:48:48 +01:00
"""Returns a list of xy pairs of the pieces where a condition is met """
2020-11-12 00:17:02 +01:00
result = []
for y, row in enumerate(self.boardArray):
for x, piece in enumerate(row):
2020-11-12 19:17:16 +01:00
try:
if condition(piece):
result.append((x, y))
2020-11-12 23:43:39 +01:00
except AttributeError: # Position is None
2020-11-12 19:17:16 +01:00
pass
2020-11-12 00:17:02 +01:00
return result
2020-11-12 23:43:39 +01:00
def checkCheck(self, color, simulation=False) -> bool:
2020-11-12 00:17:02 +01:00
"""Check whether a team is caught in check. The color is the color of the team to check"""
2020-11-12 19:17:16 +01:00
king = self.getPositionsWhere(lambda piece: piece.type == 'k' and piece.color == color)[0]
2020-11-12 00:17:02 +01:00
piecesToCheck = self.getPositionsWhere(lambda piece: piece.color != color)
2020-11-13 18:48:48 +01:00
# 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)
2020-11-12 00:17:02 +01:00
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"""
2020-11-12 19:17:16 +01:00
king = self.getPositionsWhere(lambda piece: piece.type == 'k' and piece.color == color)[0]
2020-11-12 00:17:02 +01:00
piecesToCheck = self.getPositionsWhere(lambda piece: piece.color != color)
2020-11-12 23:43:39 +01:00
# 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
2020-11-12 00:17:02 +01:00
result = []
for piece in piecesToCheck:
2020-11-12 23:43:39 +01:00
result.append([piece])
2020-11-12 00:17:02 +01:00
if self.getPieceAt(*piece).type not in ['p', 'n', 'k']:
def getDirection(fromPosition, toPosition) -> tuple:
2020-11-13 18:48:48 +01:00
"""Get the direction as a tuple from the threatening piece to the king"""
2020-11-12 00:17:02 +01:00
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)
2020-11-12 19:17:16 +01:00
def getPositionsUntilKing(x, y, direction) -> Iterable[tuple]:
2020-11-13 18:48:48 +01:00
"""Return a list of every position until the king"""
2020-11-12 00:17:02 +01:00
result = []
2020-11-12 23:43:39 +01:00
x += direction[0]
y += direction[1]
2020-11-12 00:17:02 +01:00
while self.getPieceAt(x, y) == None:
result.append((x, y))
x += direction[0]
y += direction[1]
return result
2020-11-13 18:48:48 +01:00
direction = getDirection(piece, king)
2020-11-12 23:43:39 +01:00
result[-1] += getPositionsUntilKing(*king, direction)
2020-11-12 00:17:02 +01:00
2020-11-12 23:43:39 +01:00
def getCommonValues(lst: Iterable[Iterable[tuple]]):
2020-11-13 18:48:48 +01:00
"""Combine lists so that only tuples in all the lists of threatening pieces are valid"""
2020-11-12 23:43:39 +01:00
result = set(lst[0])
for sublst in lst[1:]:
result.intersection_update(sublst)
return result
return getCommonValues(result)
2020-11-12 00:17:02 +01:00
2020-11-12 19:17:16 +01:00
def playerHasLegalMoves(self, color) -> bool:
2020-11-13 18:48:48 +01:00
""" returns whether or not a player has any legal moves left"""
2020-11-12 19:17:16 +01:00
enemyPieces = self.getPositionsWhere(lambda piece: piece.color == color)
2020-11-12 23:43:39 +01:00
if self.checkCheck(color):
getLegalMoves = lambda piece: Piece.possibleMoves(
*piece, self, legalMoves=self.getPositionsToProtectKing(color))
else:
getLegalMoves = lambda piece: Piece.possibleMoves(*piece, self)
2020-11-13 18:48:48 +01:00
return any(getLegalMoves(piece) != [] for piece in enemyPieces)
2020-11-12 19:17:16 +01:00
2020-11-12 00:17:02 +01:00
def checkStaleMate(self, color) -> bool:
"""Check whether a team is caught in stalemate. The color is the color of the team to check"""
2020-11-12 19:17:16 +01:00
return (not self.checkCheck(color)) and not self.playerHasLegalMoves(color)
2020-11-12 00:17:02 +01:00
def checkCheckMate(self, color) -> bool:
"""Check whether a team is caught in checkmate. The color is the color of the team to check"""
2020-11-12 19:17:16 +01:00
return self.checkCheck(color) and not self.playerHasLegalMoves(color)
2020-11-12 00:17:02 +01:00
2020-11-13 18:48:48 +01:00
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"""
2020-11-12 00:17:02 +01:00
x, y = position
toX, toY = toPosition
self.boardArray[toY][toX] = self.boardArray[y][x]
self.boardArray[y][x] = None
2020-11-13 18:48:48 +01:00
for x, y in piecesToRemove:
self.boardArray[y][x] = None