Untitled
unknown
python
2 years ago
19 kB
17
Indexable
import itertools
import os
import json
from gen_config import options as GAMEMODE_OPTIONS
DIM = 8
class Gamemode: # allows reference to gamemode e.g. Gamemode.default
default = "regular"
king_of_the_hill = "king of the hill"
class Position:
def __init__(self, x:int, y:int, gamestate):
self.x = x
self.y = y
self.piece_type = gamestate.get_piece(self)
@property
def pos(self) -> tuple:
return self.x, self.y
def __str__(self):
return str(self.pos)
def __eq__(self, other):
if isinstance(other, Position) and self.pos == other.pos:
return True
return False
class Move:
def __init__(self, start:Position, end:Position, gamestate):
#// self.startx, self.starty = start
#// self.endx, self.endy = end
self.start, self.end = start, end
self.start_piece = gamestate.get_piece(start)
self.end_piece = gamestate.get_piece(end)
#// self.id = int(str(self.startx)+str(self.starty)+str(self.endx)+str(self.endy))
#// def __eq__(self, other) -> bool:
#// if isinstance(other, Move) and self.id == other.id:
#// return True
#// return False
__eq__ = lambda self, other: True if isinstance(other, Move) and self.id == other.id else False # overrides the default __eq__ and compares the ids
# of the 2 Move objects
#// def __str__(self) -> str:
#// return str([self.start.piece_type, self.start.x, self.start.y, self.end.x, self.end.y, self.end.piece_type])
__str__ = lambda self: str([self.start.x, self.start.y, self.start.piece_type, self.end.x, self.end.y, self.end.piece_type])
@property
def id(self) -> str: return str(self.start.x)+str(self.start.y)+str(self.end.x) + str(self.end.y)
@property
def capturing_move(self)->bool:
inverse = {"w":"b", "b":"w", "-":"-"}
if self.start_piece[0] == inverse.get(self.end_piece[0]):
return True
return False
class State:
#NOTE: b = black, w = white
#NOTE: P = pawn, R = rook, N = knight, B = bishop, Q = queen, K = king
#NOTE w- and b- are placeholder can used for en passant
piece_codes:tuple = ("--", "b-", "w-", "wP", "wR", "wN", "wB", "wQ", "wK", "bP", "bR", "bN", "bB", "bQ", "bK")
blank:str = "--"
def __init__(self, custom_board_flag=False,
gamemode=GAMEMODE_OPTIONS[0],
show_all_valid_moves_flag=False,
timer_min=0,
timer_sec=0,
resolution=400,
):
self.board = [["--" for i in range(8)] for j in range(8)]
fg:bool = False
if custom_board_flag:
fg = self.load_board(True)
if not fg:
self.board = [
["bR", "bN", "bB", "bK", "bQ", "bB", "bN", "bR"],
["bP", "bP", "bP", "bP", "bP", "bP", "bP", "bP"],
["--", "--", "--", "--", "--", "--", "--", "--"],
["--", "--", "--", "--", "--", "--", "--", "--"],
["--", "--", "--", "--", "--", "--", "--", "--"],
["--", "--", "--", "--", "--", "--", "--", "--"],
["wP", "wP", "wP", "wP", "wP", "wP", "wP", "wP"],
["wR", "wN", "wB", "wK", "wQ", "wB", "wN", "wR"]
]
self.validate_board()
self.white_move:bool = True
self.move_log:list = []
self.moves:list = []
self._temp_moves:list = []
#from config file
self.custom_board_flag = custom_board_flag
self.gamemode = gamemode
self.show_all_valid_moves_flag = show_all_valid_moves_flag
self.timer_min = timer_min
self.timer_sec = timer_sec
self.resolution = resolution
self.checkmate = False
#! Asumes only 1 king of each colour on board
self.white_king_pos = self.get_white_king_pos() # tuple of Position objs
self.black_king_pos = self.get_black_king_pos() # tuple of Position objs
#! this line MUST go last in init
self.moves = self.get_valid_moves() # list of all possible moves (ex checks) for 1 move
def load_board(self, flag:bool, filename=os.path.join("boards", "board.json")) -> bool:
if flag:
try:
with open(filename, "r") as f:
data = json.loads(f.read())
self.board = data
return True
except FileNotFoundError:
print("failed to load custom board layout, using default")
# return False
except ValueError:
print("board invalid, using default")
# return False
except IndexError:
print("board invalid, using default")
return False
def validate_board(self)->None:
for y in range(DIM):
for x in range(DIM):
if self.get_piece(Position(x,y,self)) not in self.piece_codes:
raise ValueError(f"\"{self.get_piece(Position(x,y,self))}\" is not a valid piece code")
@property
def moves_ids(self)->list:
return [self.moves[i].id for i in range(len(self.moves))]
def get_white_king_pos(self)->tuple:
arr:list = []
# for y_idx, y in enumerate(self.board):
# for x_idx, x in enumerate(y):
for y_idx in range(DIM):
for x_idx in range(DIM):
if self.get_piece(Position(x_idx, y_idx, self)) == "wK":
return Position(x_idx, y_idx, self)
def get_black_king_pos(self)->Position:
pos:Position
for y_idx, y in enumerate(self.board):
for x_idx, x in enumerate(y):
if self.get_piece(Position(x_idx, y_idx, self)) == "bK":
return Position(x_idx, y_idx, self)
def update_king_pos(self)->None:
self.white_king_pos = self.get_white_king_pos()
self.black_king_pos = self.get_black_king_pos()
def undo_prev_move(self) -> None:
if len(self.move_log) > 0:
x:Move=self.move_log.pop()
self.set_piece(x.start, x.start.piece_type)
self.set_piece(x.end, x.end.piece_type)
self.white_move = not self.white_move
self.update_king_pos()
# self.moves = self.get_valid_moves()
def winner(self) -> str:
if self.checkmate:
return "White" if not self.white_move else "Black"
# def get_piece(self, pos_obj: Position):
# return self.board[pos_obj.y][pos_obj.x]
get_piece = lambda self, pos_obj: self.board[pos_obj.y][pos_obj.x]
def set_piece(self, pos_obj:Position, change_to:str) -> None:
if pos_obj.piece_type in self.piece_codes:
self.board[pos_obj.y][pos_obj.x] = change_to
else:
raise ValueError("set_piece given invalid piece type")
# Unused
#// def is_move_valid(self, move_obj:Move) -> bool:
#// # check if player is trying to capture their own colour
#// is_own_colour = lambda self, move_obj: False if move_obj.start.piece_type[0] == move_obj.end.piece_type[0] else True
#// # for i in self.get_all_valid_moves_ex_checks():
#// # print(i)
#// return is_own_colour(self, move_obj) and (move_obj in self.get_all_valid_moves_ex_checks())
def move_piece(self, move_obj:Move, arr:list) -> None:
#// start, end = move_obj.start, move_obj.end
#// piece_type, captured_type = self.get_piece(start), self.get_piece(end)
if move_obj in arr:
# print(move_obj)
self.set_piece(move_obj.end, move_obj.start.piece_type) # replace enemy/black space with payers piece
self.set_piece(move_obj.start, State.blank) # set moved piece to empty
self.white_move = not self.white_move
self.move_log.append(move_obj)
self.update_king_pos()
# self.moves = self.get_valid_moves()
def move_piece_by_id(self, id)->None:
x1,y1,x2,y2 = id
self.move_piece(Move(Position(int(x1), int(y1), self), Position(int(x2), int(y2), self), self), self.moves)
def get_valid_moves(self) -> list:
copy_white_move = self.white_move
self.moves.clear()
# generate all possible moves
arr:list=[]
arr = self.get_all_valid_moves_ex_checks(arr) # appends moves to arr
#! FIXED --------------------------------------------------------------------------------------------------
#// arr.append(Move(Position(0,0,self), Position(5,5,self), self))
# bad fix, the last elements is handled incorrectly
# append random move to the end of the list
# then pop it off on line the 2nd last line of the function
#// arr.pop(); return arr
# NOTE uncommenting this line removes the king in check check
# NOTE also that when uncommented checkmate doesnt work and the game will only end when no piece can make a move
#! FIXED --------------------------------------------------------------------------------------------------
# for each move make move
self.white_move = not self.white_move
for i in range(len(arr)-1, -1, -1): # looping backward as removing from list, prevents shifting indexes
self.move_piece(arr[i], arr)
# self.white_move = not self.white_move
if self.is_checked(): # generates oppenents moves
arr.remove(arr[i]) # remove invalid moves
self.undo_prev_move() # undo temporary moves
self.white_move = not copy_white_move # make sure white_move var is set correctly
self.white_move = copy_white_move
#// arr.pop()
return arr
def is_checked(self) -> bool:
self.update_king_pos()
if self.is_attacked_position(self.white_king_pos) or self.is_attacked_position(self.black_king_pos):
return True
#// else:
#// if self.is_attacked_position(self.black_king_pos):
#// return True
#// self.white_move = not self.white_move
return False
def is_attacked_position(self, pos:Position)->bool:
self.white_move = not self.white_move # to generate oppenents move
opponents_moves = self.get_all_valid_moves_ex_checks(list())
self.white_move = not self.white_move # switch back
op_mov:Move
for op_mov in opponents_moves:
#// print(op_mov.end)
if op_mov.end == pos:
return True
return False
def get_all_valid_moves_ex_checks(self, arr:list) -> None:
#// move_arr:list = []
arr.clear()
for y in range(DIM):
for x in range(DIM):
p = Position(x, y, self).piece_type
piece_colour, piece_type = p
#// print(piece_colour+piece_type)
if (piece_colour == "w" and self.white_move) or (piece_colour == "b" and not self.white_move):
# checking the Piece type and generating the correct move set for it
match piece_type: # match statement added in python 3.10
case "-": # null char, used for en passand
continue
case "P":
self.get_pawn_moves(x, y, arr)
case "R":
self.get_rook_moves(x, y, arr)
case "N":
self.get_knight_moves(x, y, arr)
case "B":
self.get_bishop_moves(x, y, arr)
case "Q":
self.get_queen_moves(x, y, arr)
case "K":
self.get_king_moves(x, y, arr)
case default: # piece not valid
raise ValueError(f"invalid piece: \"{piece_type}\"")
return arr
# helper function
def generic_piece_move(self, x:int, y:int, matrix:tuple, arr:list, distance:int=7, no_jumps=True)->None:
# var no_jumps is technically unused, but allow easy modification so im leaving it in
distance+=1 # add to include the square the piece is on, to make more intuitive
if not (0<distance<=8): # range check
raise ValueError(f"distance is out of range and cant be {distance}")
for i in matrix:
if not isinstance(i, tuple):
raise ValueError(f"{i} must be tuple")
enemy_type:str = "b" if self.white_move else "w"
m:tuple; i:int; endx:int; endy:int
for m in matrix:
for i in range(1, distance):
endx, endy = x + m[0] * i, y + m[1] * i # perform vector move on piece and
#increment vector magnitude based on piece max distance (normally 1 or 8)
if 0<=endx<8 and 0<=endy<8: #range check
target_piece = self.get_piece(Position(endx, endy, self))
if target_piece == State.blank:
arr.append(Move(Position(x,y,self), Position(endx, endy,self), self))
elif target_piece[0]==enemy_type:
arr.append(Move(Position(x,y,self), Position(endx, endy,self), self))
if no_jumps:
break # prevent piece from jumping pieces
else: # cant take white piece
if no_jumps:
break
else:
continue
# // def get_pawn_moves(self, x:int, y:int): #//NOTE: have to do pawns taking on diagonals
# // piece_infront: list = []
# // if self.white_move and Position(x, y ,self).piece_type == "wP":
# // piece_infront.append(Position(x, y-1, self))
# // if y == 6:
# // piece_infront.append(Position(x, y-2, self))
# // # if self.get_piece(piece_infront) == State.blank:
# // # self.moves.append(Move(Position(x, y, self), piece_infront, self))
# // else:
# // piece_infront.append(Position(x, y+1, self))
# // if y == 1:
# // piece_infront.append(Position(x, y+2, self))
# // for i in piece_infront:
# // if i.piece_type == State.blank:
# // self.moves.append(Move(Position(x, y, self), i, self))
# // for i in self.moves:
# // print(i)
def get_pawn_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
# NOTE should refactor this code :)
diagonals = ((1,1), (-1, 1), (1,-1), (-1,-1))
if self.white_move:
try:
if self.get_piece(Position(x,y-1, self)) == State.blank:
arr.append(Move(Position(x,y,self), Position(x,y-1,self), self))
if self.get_piece(Position(x,y-2, self)) == State.blank and y == 6:
arr.append(Move(Position(x,y,self), Position(x,y-2,self), self))
except IndexError:
pass
# check white diagonals
for i in range(2,4):
try:
a,b = x + diagonals[i][0],y + diagonals[i][1]
if a >= 0 and b >= 0:
if self.get_piece(Position(a, b, self))[0] == "b":
arr.append(Move(Position(x,y,self), Position(a, b, self), self))
except IndexError:
pass
else:
try:
if self.get_piece(Position(x,y+1, self)) == State.blank:
arr.append(Move(Position(x,y,self), Position(x,y+1,self), self))
if self.get_piece(Position(x,y+2, self)) == State.blank and y == 1:
arr.append(Move(Position(x,y,self), Position(x,y+2,self), self))
except IndexError:
pass
# checks blacks diagonals
for i in range(2):
try:
a,b = x + diagonals[i][0],y + diagonals[i][1]
if a >= 0 and b >= 0:
if self.get_piece(Position(a, b, self))[0] == "w":
arr.append(Move(Position(x,y,self), Position(a, b, self), self))
except IndexError:
pass
def get_rook_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
matrix = ((1, 0), (-1, 0), (0, 1), (0, -1))
self.generic_piece_move(x, y, matrix, arr)
def get_bishop_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
matrix:tuple = ((1, 1), (-1, -1), (-1, 1), (1, -1))
self.generic_piece_move(x, y, matrix, arr)
def get_knight_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
matrix = ((2,1), (2,-1), (-2,1), (-2,-1), (1,-2), (1,2), (-1, -2), (-1, 2))
self.generic_piece_move(x,y,matrix,arr, distance=1, no_jumps=False) #NOTE: no_jumps doesnt change to behavour in this instance as
# it only has a dist of 1 so technically makes no jumps in the
# code, due to the matrix moving the knight by +-2 and +-1 in
# the x or y.
#
# a "jump" is defined by the number of times the matrix
# is applied to the piece
def get_king_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
iterations = [1, 0, -1]
test = list(itertools.permutations(iterations, 2))
test.append((1,1)); test.append((-1,-1))
self.generic_piece_move(x, y, tuple(test), arr, distance=1)
def get_queen_moves(self, x:int, y:int, arr:list) -> None: # moves appended to arr
iterations = [1, 0, -1]
test = list(itertools.permutations(iterations, 2)) # gets all permutations but not repeats e.g. (1,1) etc
test.append((1,1)); test.append((-1,-1)) # add missed permutations
self.generic_piece_move(x, y, tuple(test), arr)
Editor is loading...
Leave a Comment