From 022cb43c92882f1ba256463caf238e18fc440704 Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Fri, 17 Jun 2022 19:30:09 -0400 Subject: [PATCH] You can't take the king :) --- .gitignore | 2 + chess.py | 166 +++++++++++++++++++++++++++++++++++++++++++++++++++++ vector.py | 45 +++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 .gitignore create mode 100644 chess.py create mode 100644 vector.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/chess.py b/chess.py new file mode 100644 index 0000000..d45a375 --- /dev/null +++ b/chess.py @@ -0,0 +1,166 @@ +#!/bin/python + +import random +from collections import namedtuple +from vector import vec2, vec2_from_text + +Piece = namedtuple('Piece', 'side rank') + +ranks = 'RNBQKBNR' + +def next_turn(turn): + return 'B' if turn == 'W' else 'W' + +def create_board(): + whites = {} + whites.update({vec2(c, 0): rank for (c, rank) in zip(range(8), ranks)}) + whites.update({vec2(c, 1): 'P' for c in range(8)}) + + blacks = {} + blacks.update({vec2(c, 7): rank for (c, rank) in zip(range(8), ranks)}) + blacks.update({vec2(c, 6): 'P' for c in range(8)}) + + return whites, blacks + +def print_board(whites, blacks): + for y in range(7, -1, -1): + print(str(y + 1) + ' ', end='') + for x in range(8): + pos = vec2(x, y) + if pos in whites: + print('+' + whites[pos] + '+ ', end='') + elif pos in blacks: + print('-' + blacks[pos] + '- ', end='') + else: + print(' ', end='') + print() + print(' ' + ' '.join('ABCDEFGH')) + +def get_human_move(turn): + s = input(turn + ' to move: ') + return tuple(map(vec2_from_text, s.split())) + +def get_computer_move(turn, whites, blacks): + return random.choice(get_valid_moves(turn, whites, blacks)) + +def get_valid_dsts(src, whites, blacks): + # todo: castling + # todo: en passant + is_white_turn = src in whites + self_pieces, oppo_pieces = (whites, blacks) if is_white_turn else (blacks, whites) + + piece = self_pieces[src] + valid_dsts = set() + + straight_dirs = set([vec2(1, 0), vec2(-1, 0), vec2(0, 1), vec2(0, -1)]) + diagonal_dirs = set([vec2(1, 1), vec2(-1, 1), vec2(1, -1), vec2(-1, -1)]) + def add_dirs(dirs, *, single=False): + for dir in dirs: + delta = dir + while (src + delta).is_valid(): + dst = src + delta + if dst in self_pieces: + break + valid_dsts.add(dst) + if dst in oppo_pieces: + break + if single: + break + delta += dir + + if piece == 'P': # Pawn + dy = 1 if is_white_turn else -1 + sy = 1 if is_white_turn else 6 + dst_fw1 = src + vec2(0, dy) + dst_fw2 = src + vec2(0, dy * 2) + dst_tkl = src + vec2(-1, dy) + dst_tkr = src + vec2(1, dy) + if dst_fw1.is_valid() and dst_fw1 not in self_pieces and dst_fw1 not in oppo_pieces: + valid_dsts.add(dst_fw1) + if src.y == sy and dst_fw2 not in self_pieces and dst_fw2 not in oppo_pieces: + valid_dsts.add(dst_fw2) + if dst_tkl in oppo_pieces: + valid_dsts.add(dst_tkl) + if dst_tkr in oppo_pieces: + valid_dsts.add(dst_tkr) + elif piece == 'N': # Knight + knight_dirs = set([vec2(1, 2), vec2(2, 1), vec2(2, -1), vec2(1, -2), vec2(-1, -2), vec2(-2, -1), vec2(-2, 1), vec2(-1, 2)]) + add_dirs(knight_dirs, single=True) + elif piece == 'R': # Rook + add_dirs(straight_dirs) + elif piece == 'B': # Bishop + add_dirs(diagonal_dirs) + elif piece == 'Q': # Queen + add_dirs(straight_dirs | diagonal_dirs) + elif piece == 'K': # King + add_dirs(straight_dirs | diagonal_dirs, single=True) + + return valid_dsts + +def get_valid_moves(turn, whites, blacks): + self_pieces = whites if turn == 'W' else blacks + return [(src, dst) for src in self_pieces for dst in get_valid_dsts(src, whites, blacks)] + +def next_board(src, dst, whites, blacks): + self_pieces, oppo_pieces = (whites, blacks) if src in whites else (blacks, whites) + self_pieces[dst] = self_pieces[src] # Move src -> dst + del self_pieces[src] # Remove src + if dst in oppo_pieces: # Remove dst if it was an opponent + del oppo_pieces[dst] + return whites, blacks + +# Valid moves filtered such that the opponent cannot take the current player's king +def get_legal_moves(turn, whites, blacks): + self_pieces, oppo_pieces = (whites, blacks) if turn == 'W' else (blacks, whites) + valid_moves = get_valid_moves(turn, whites, blacks) + legal_moves = [] + for src, dst in valid_moves: + n_whites, n_blacks = next_board(src, dst, whites.copy(), blacks.copy()) + n_turn = next_turn(turn) + n_self_pieces, n_oppo_pieces = (n_whites, n_blacks) if n_turn == 'W' else (n_blacks, n_whites) + n_valid_moves = get_valid_moves(n_turn, whites, blacks) + legal = True + for n_src, n_dst in n_valid_moves: + if n_dst in n_oppo_pieces and n_oppo_pieces[n_dst] == 'K': + legal = False + break + if legal: + legal_moves.append((src, dst)) + return legal_moves + +def is_in_check(turn, whites, blacks): + self_pieces, oppo_pieces = (whites, blacks) if turn == 'W' else (blacks, whites) + n_turn = next_turn(turn) + threat_moves = get_valid_moves(n_turn, whites, blacks) + for src, dst in threat_moves: + if dst in self_pieces and self_pieces[dst] == 'K': + return True + return False + +turn = 'W' +whites, blacks = create_board() +while True: + print(turn, 'to move') + print_board(whites, blacks) + legal_moves = get_legal_moves(turn, whites, blacks) + print(len(legal_moves), 'valid moves') + for src, dst in legal_moves: + print(src, '->', dst) + if len(legal_moves) == 0: + print('game is over') + break +# if turn == 'W': + while True: + try: + src, dst = get_human_move(turn) + if not (src, dst) in legal_moves: + print('invalid move') + else: + break + except ValueError: + print('invalid input') +# else: +# src, dst = get_computer_move(turn, whites, blacks) + + whites, blacks = next_board(src, dst, whites, blacks) + turn = next_turn(turn) diff --git a/vector.py b/vector.py new file mode 100644 index 0000000..0ca8709 --- /dev/null +++ b/vector.py @@ -0,0 +1,45 @@ +rows = '12345678' +cols = 'ABCDEFGH' + +#class vec2: +# def __init__(self, x, y): +# self.x = int(x) +# self.y = int(y) +# def __add__(self, other): +# return vec2(self.x + other.x, self.y + other.y) +# def __sub__(self, other): +# return vec2(self.x - other.x, self.y - other.y) +# def __mul__(self, scalar): +# return vec2(self.x * scalar, self.y * scalar) +# def __repr__(self): +# return 'vec2({},{})'.format(self.x, self.y) +# def __eq__(self, other): +# return self.x == other.x and self.y == other.y + +class vec2: + def __init__(self, x, y): + x, y = int(x), int(y) +# if not 0 <= x < 8: +# raise Exception('invalid chessvec x: {}'.format(repr(x))) +# if not 0 <= y < 8: +# raise Exception('invalid chessvec y: {}'.format(repr(y))) + self.x = x + self.y = y + def __add__(self, other): # Add vec2 + return vec2(self.x + other.x, self.y + other.y) + def __sub__(self, other): # Subtract vec2 + return vec2(self.x - other.x, self.y - other.y) + def __eq__(self, other): + return self.x == other.x and self.y == other.y + def __hash__(self): + return hash(self.x) + hash(self.y) + def __repr__(self): + return 'vec2({},{})'.format(self.x, self.y) + def __str__(self): + return cols[self.x] + rows[self.y] if self.is_valid() else repr(self) + def is_valid(self): + return 0 <= self.x < 8 and 0 <= self.y < 8 + +def vec2_from_text(s): + return vec2(cols.index(s[0]), rows.index(s[1])) +