more cool stuff
chess, woah
This commit is contained in:
164
chess/chess_gui_small_view.py
Normal file
164
chess/chess_gui_small_view.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import copy
|
||||
from enum import Enum
|
||||
import pygame as pg
|
||||
import pygame_gui as gui
|
||||
from chess_model import ChessModel, MoveValidity, UndoException
|
||||
from move import Move
|
||||
from player import Player
|
||||
|
||||
IMAGE_SIZE = 52 #small format - images 52 X 52
|
||||
|
||||
|
||||
class SpriteType(Enum):
|
||||
King = 0
|
||||
Queen = 1
|
||||
Bishop = 2
|
||||
Knight = 3
|
||||
Rook = 4
|
||||
Pawn = 5
|
||||
|
||||
class SpriteColor(Enum):
|
||||
WHITE = 0
|
||||
BLACK = 1
|
||||
|
||||
class GUI:
|
||||
first = True
|
||||
def __init__(self) -> None:
|
||||
pg.init()
|
||||
self.__model = ChessModel()
|
||||
self._screen = pg.display.set_mode((800, 600))
|
||||
pg.display.set_caption("Laker Chess")
|
||||
self._ui_manager = gui.UIManager((800, 600))
|
||||
self._side_box = gui.elements.UITextBox('<b>Laker Chess</b><br /><br />White moves first.<br />',
|
||||
relative_rect=pg.Rect((500, 100), (400, 500)),
|
||||
manager=self._ui_manager)
|
||||
self._undo_button = gui.elements.UIButton(relative_rect = pg.Rect((700, 50), (100, 50)),
|
||||
text='Undo',
|
||||
manager=self._ui_manager)
|
||||
self._restart_button = gui.elements.UIButton(relative_rect = pg.Rect((600, 50), (100, 50)),
|
||||
text='Reset',
|
||||
manager=self._ui_manager)
|
||||
self._piece_selected = False
|
||||
self._first_selected = (0, 0)
|
||||
self._second_selected = (0, 0)
|
||||
|
||||
@classmethod
|
||||
def load_images(cls):
|
||||
def load_image(color, ptype):
|
||||
SS = pg.image.load('./images/pieces.png')
|
||||
a = 105
|
||||
surf = pg.Surface((a,a), pg.SRCALPHA)
|
||||
surf.blit(SS, (0, 0), pg.rect.Rect(a*ptype.value, color.value*a, a, a))
|
||||
surf_scaled = pg.transform.scale(surf, (IMAGE_SIZE, IMAGE_SIZE))
|
||||
return surf_scaled
|
||||
cls.white_sprites = {}
|
||||
cls.black_sprites = {}
|
||||
for st in SpriteType:
|
||||
cls.white_sprites[st.name] = load_image(SpriteColor.WHITE, st)
|
||||
cls.black_sprites[st.name] = load_image(SpriteColor.BLACK, st)
|
||||
|
||||
def run_game(self) -> None:
|
||||
running = True
|
||||
time_delta = 0
|
||||
clock = pg.time.Clock()
|
||||
while running:
|
||||
for event in pg.event.get():
|
||||
if event.type == pg.QUIT:
|
||||
running = False
|
||||
if event.type == pg.MOUSEBUTTONDOWN:
|
||||
x, y = pg.mouse.get_pos()
|
||||
y, x = self.__get_coords__(y, x)
|
||||
piece = self.__model.piece_at(y, x)
|
||||
if not self._piece_selected and piece:
|
||||
if piece.player != self.__model.current_player:
|
||||
msg = 'Not your turn!'
|
||||
self._side_box.append_html_text(msg + '<br />')
|
||||
else:
|
||||
self._piece_selected = True
|
||||
self._first_selected = y, x
|
||||
self._piece_selected = piece
|
||||
elif self._piece_selected:
|
||||
mv = Move(self._first_selected[0], self._first_selected[1], y, x)
|
||||
if self.__model.is_valid_move(mv):
|
||||
target = self.__model.piece_at(y, x)
|
||||
self.__model.move(mv)
|
||||
if target is not None:
|
||||
msg = f'Moved {self._piece_selected} and captured {target}'
|
||||
else:
|
||||
msg = f'Moved {self._piece_selected}'
|
||||
self._side_box.append_html_text(msg + '<br />')
|
||||
|
||||
else:
|
||||
self._side_box.append_html_text(f'{self.__model.messageCode}<br />')
|
||||
incheck = self.__model.in_check(self.__model.current_player)
|
||||
complete = self.__model.is_complete()
|
||||
|
||||
if incheck:
|
||||
player_color = self.__model.current_player.name
|
||||
if complete:
|
||||
self._side_box.append_html_text(f'{player_color} is in CHECKMATE!<br />GAME OVER!')
|
||||
else:
|
||||
self._side_box.append_html_text(f'{player_color} is in CHECK!<br />')
|
||||
|
||||
self._piece_selected = False
|
||||
else:
|
||||
self._piece_selected = False
|
||||
if event.type == gui.UI_BUTTON_PRESSED:
|
||||
if event.ui_element == self._restart_button:
|
||||
self.__model = ChessModel()
|
||||
self._side_box.set_text("Restarting game...<br />")
|
||||
if event.ui_element == self._undo_button:
|
||||
try:
|
||||
self.__model.undo()
|
||||
self._side_box.append_html_text('Undoing move.<br />')
|
||||
except UndoException as e:
|
||||
self._side_box.append_html_text(f'{e}<br />')
|
||||
self._ui_manager.process_events(event)
|
||||
|
||||
self._screen.fill((255, 255, 255))
|
||||
self.__draw_board__()
|
||||
self._ui_manager.draw_ui(self._screen)
|
||||
self._ui_manager.update(time_delta)
|
||||
|
||||
pg.display.flip()
|
||||
time_delta = clock.tick(30) / 1000.0
|
||||
|
||||
def __get_coords__(self, y, x):
|
||||
grid_x = x // IMAGE_SIZE
|
||||
grid_y = y // IMAGE_SIZE
|
||||
return grid_y, grid_x
|
||||
|
||||
def __draw_board__(self) -> None:
|
||||
count = 0
|
||||
color = (255, 255, 255)
|
||||
for x in range(0, 8):
|
||||
for y in range(0, 8):
|
||||
if count % 2 == 0:
|
||||
color = (255, 255, 255)
|
||||
else:
|
||||
color = (127, 127, 127)
|
||||
count = count + 1
|
||||
pg.draw.rect(self._screen, color, pg.rect.Rect(x * IMAGE_SIZE, y * IMAGE_SIZE, IMAGE_SIZE, IMAGE_SIZE))
|
||||
if self._piece_selected and (y, x) == self._first_selected:
|
||||
pg.draw.rect(self._screen, (255, 0, 0), pg.rect.Rect(x * IMAGE_SIZE, y * IMAGE_SIZE, IMAGE_SIZE, IMAGE_SIZE), 2)
|
||||
draw_piece = self.__model.piece_at(y, x)
|
||||
if draw_piece is not None:
|
||||
if draw_piece.player == Player.BLACK:
|
||||
d = GUI.black_sprites
|
||||
else:
|
||||
d = GUI.white_sprites
|
||||
self._screen.blit(copy.deepcopy(d[draw_piece.type()]), (x * IMAGE_SIZE, y * IMAGE_SIZE))
|
||||
count = count + 1
|
||||
pg.draw.line(self._screen, (0, 0, 0), (0, 840), (840, 840))
|
||||
pg.draw.line(self._screen, (0, 0, 0), (840, 840), (840, 0))
|
||||
GUI.first = False
|
||||
|
||||
|
||||
def main():
|
||||
GUI.load_images()
|
||||
g = GUI()
|
||||
g.run_game()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
35
chess/chess_model.py
Normal file
35
chess/chess_model.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from enum import Enum
|
||||
from player import Player
|
||||
from move import Move
|
||||
from chess_piece import ChessPiece
|
||||
from pawn import Pawn
|
||||
from rook import Rook
|
||||
from knight import Knight
|
||||
from bishop import Bishop
|
||||
from queen import Queen
|
||||
from king import King
|
||||
from move import Move
|
||||
|
||||
class MoveValidity(Enum):
|
||||
Valid = 1
|
||||
Invalid = 2
|
||||
MovingIntoCheck = 3
|
||||
StayingInCheck = 4
|
||||
|
||||
def __str__(self):
|
||||
if self.value == 2:
|
||||
return 'Invalid move.'
|
||||
|
||||
if self.value == 3:
|
||||
return 'Invalid -- cannot move into check.'
|
||||
|
||||
if self.value == 4:
|
||||
return 'Invalid -- must move out of check.'
|
||||
|
||||
|
||||
# TODO: create UndoException
|
||||
|
||||
|
||||
class ChessModel:
|
||||
# TODO: fill in this class
|
||||
pass
|
||||
52
chess/chess_piece.py
Normal file
52
chess/chess_piece.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from player import Player
|
||||
from move import Move
|
||||
from typing import TypeVar
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
ChessPieceT = TypeVar('ChessPieceT')
|
||||
|
||||
# my list of custom exceptions
|
||||
class PieceOutOfBoundsError(Exception): pass
|
||||
class StartEndPositionMismatch(Exception): pass
|
||||
|
||||
class ChessPiece:
|
||||
def __init__(self, piece_color: Player):
|
||||
self.player = piece_color
|
||||
|
||||
@property
|
||||
def player(self):
|
||||
return self.__player
|
||||
|
||||
@player.setter
|
||||
def player(self, new_val):
|
||||
if not isinstance(new_val, Player):
|
||||
raise TypeError(f'new value for player is not of type Player')
|
||||
self.__player = new_val
|
||||
|
||||
def __str__(self):
|
||||
# im not making this abstract, attributes amongst each piece are the same, str repr is also dynamic for the class name
|
||||
return f'[{self.__class__.__name__} player={self.player}]'
|
||||
|
||||
def is_valid_move(self, move: Move, board: list[list[ChessPieceT]]) -> bool:
|
||||
if not isinstance(board, list):
|
||||
raise TypeError(f'board must be a list')
|
||||
|
||||
for arr in board:
|
||||
if not isinstance(arr, list):
|
||||
raise TypeError(f'each element in the board list bust be another list')
|
||||
|
||||
for v in arr:
|
||||
if not isinstance(v, ChessPiece):
|
||||
raise TypeError(f'each element in each row of the board must be of type ChessPiece')
|
||||
|
||||
board_dim = len(board)
|
||||
board_orig: ChessPiece = board[move.to_row][move.to_col]
|
||||
board_dest: ChessPiece = board[move.from_row][move.from_col]
|
||||
within_bounds = board_dim <= move.to_col <= board_dim and board_dim <= move.to_row <= board_dim
|
||||
different_position = move.from_col != move.to_col and move.from_row != move.to_row
|
||||
at_position = board_orig == self
|
||||
is_piece_class = isinstance(board_dest, ChessPiece)
|
||||
taking_friendly_piece = board_dest.player != self.player
|
||||
|
||||
print(f'within_bounds={within_bounds}, different_position={different_position}, at_position={at_position}, is_piece_class={is_piece_class}, taking_friendly_piece={taking_friendly_piece}')
|
||||
return within_bounds and different_position and at_position and is_piece_class and taking_friendly_piece
|
||||
BIN
chess/images/pieces.png
Normal file
BIN
chess/images/pieces.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
chess/images/small_pieces.png
Normal file
BIN
chess/images/small_pieces.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
121
chess/move.py
Normal file
121
chess/move.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from enum import Enum
|
||||
|
||||
class Move:
|
||||
def __init__(self, from_row, from_col, to_row, to_col):
|
||||
self.from_row = from_row
|
||||
self.from_col = from_col
|
||||
self.to_row = to_row
|
||||
self.to_col = to_col
|
||||
|
||||
def __str__(self):
|
||||
output = f'Move [from_row={self.from_row}, from_col={self.from_col}'
|
||||
output += f', to_row={self.to_row}, to_col={self.to_col}]'
|
||||
return output
|
||||
|
||||
# kinda just guessing, but on prarielearn it only showed the file names included in the project and the
|
||||
# ones we need to create for the submission, i was gonna put all these in their own files, but now i'm
|
||||
# just putting it here because i dont wanna risk not being able to submit
|
||||
|
||||
class PieceType:
|
||||
# piece type for each piece
|
||||
PAWN = 0
|
||||
ROOK = 1
|
||||
KING = 2
|
||||
QUEEN = 3
|
||||
KNIGHT = 4
|
||||
BISHOP = 5
|
||||
|
||||
# check if something is a valid move set element ( (y, x) tuple )
|
||||
def valid_move_set_element(move_set: tuple[int, int]) -> bool:
|
||||
# check if move set is a tuple
|
||||
if not isinstance(move_set, tuple):
|
||||
raise TypeError(f'each move set in move sets must be a tuple ({move_set})')
|
||||
|
||||
# check if the length of the tuple is 2, because it needs to have a y and x
|
||||
ms_len = len(move_set)
|
||||
if ms_len != 2:
|
||||
raise ValueError(f'length of move set ({move_set}) is {ms_len}, must be 2 (y and x)')
|
||||
|
||||
# check if each element is an int
|
||||
for i in range(ms_len):
|
||||
p = move_set[i]
|
||||
if not isinstance(p, int):
|
||||
raise TypeError(f'tuple element at index {i} ({p}) must be an int')
|
||||
|
||||
return True
|
||||
|
||||
# general move set list class for each piece type
|
||||
class MoveSets:
|
||||
def __init__(self, *move_sets: tuple[int, int]):
|
||||
# loop over indices of move_sets, checking if each element at index i is valid, exception thrown by valid_move_set if not
|
||||
for i in range(len(move_sets)):
|
||||
valid_move_set_element(move_sets[i])
|
||||
|
||||
# set all the stuff equal
|
||||
self.__move_sets = move_sets
|
||||
|
||||
@property
|
||||
def move_sets(self):
|
||||
return self.__move_sets
|
||||
|
||||
def is_valid_move(self, move: Move) -> bool:
|
||||
# is the move valid, i dunno
|
||||
raise NotImplementedError('u gotta implement me bruh')
|
||||
|
||||
def valid_range(max: int) -> list[int]:
|
||||
return [-i if max < 0 else i for i in range(1, abs(max)+1)]
|
||||
|
||||
# class for static move sets
|
||||
class StaticMoveSet(MoveSets):
|
||||
def is_valid_move(self, move: Move) -> bool:
|
||||
from_to_row_diff = move.to_row - move.from_row
|
||||
from_to_col_diff = move.to_col - move.from_col
|
||||
for ms in self.move_sets:
|
||||
if from_to_row_diff == ms[0] and from_to_col_diff == ms[1]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# in these lists, the move sets are dynamic, so the y and x are a range of times they can move on the x and y
|
||||
rook_valid_move_sets = [(0, 8), (8, 0), (0, -8), (-8, 0)]
|
||||
|
||||
# class for dynamic move sets
|
||||
class DynamicMoveSet(MoveSets):
|
||||
def is_valid_move(self, move: Move) -> bool:
|
||||
from_to_row_diff = move.to_row - move.from_row
|
||||
from_to_col_diff = move.to_col - move.from_col
|
||||
# check if the to and from actually moved
|
||||
if from_to_row_diff == 0 and from_to_col_diff == 0:
|
||||
return False
|
||||
|
||||
for ms in self.move_sets:
|
||||
possible_valid_row_moves, possible_valid_col_moves = [valid_range(mse) for mse in ms]
|
||||
|
||||
# check if move in row is possible, only if there are valid moves for row movement
|
||||
if len(possible_valid_row_moves) > 0:
|
||||
if from_to_row_diff not in possible_valid_row_moves:
|
||||
continue
|
||||
else:
|
||||
# if theres no valid moves for rows, make sure theres no change in the from to row difference
|
||||
if from_to_row_diff != 0:
|
||||
continue
|
||||
|
||||
# check if move in column is possible, only if there are valid moves for column movement
|
||||
if len(possible_valid_col_moves) > 0:
|
||||
if from_to_col_diff not in possible_valid_col_moves:
|
||||
continue
|
||||
else:
|
||||
# if theres no valid moves for columns, make sure theres no change in the from to column difference
|
||||
if from_to_col_diff != 0:
|
||||
continue
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# create move sets
|
||||
|
||||
# static move sets
|
||||
pawn_move_sets = StaticMoveSet((0, 1), (0, 2))
|
||||
|
||||
# dynamic move sets
|
||||
12
chess/pawn.py
Normal file
12
chess/pawn.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from chess_piece import ChessPiece
|
||||
from move import Move
|
||||
from player import Player
|
||||
from move_sets import pawn_valid_move_sets
|
||||
|
||||
class Pawn(ChessPiece):
|
||||
def __init__(self, piece_color: Player):
|
||||
super().__init__(piece_color)
|
||||
|
||||
def is_valid_move(self, move: Move, board: list[list[ChessPiece]]) -> bool:
|
||||
# run original check and other piece specific checks
|
||||
orig_is_valid = super().is_valid_move(move, board)
|
||||
14
chess/player.py
Normal file
14
chess/player.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from enum import Enum
|
||||
|
||||
class Player(Enum):
|
||||
BLACK = 0
|
||||
WHITE = 1
|
||||
|
||||
def next(self):
|
||||
cls = self.__class__
|
||||
members = list(cls)
|
||||
index = members.index(self) + 1
|
||||
if index >= len(members):
|
||||
index = 0
|
||||
return members[index]
|
||||
|
||||
104
chess/test.py
Normal file
104
chess/test.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from chess_piece import ChessPiece
|
||||
from pytest import fixture, mark
|
||||
from player import Player
|
||||
from move import StaticMoveSet, Move, DynamicMoveSet, valid_range
|
||||
from random import randint, choice
|
||||
|
||||
# chess piece tests
|
||||
|
||||
@fixture
|
||||
def valid_piece():
|
||||
return ChessPiece(Player.WHITE)
|
||||
|
||||
def test_update_player(valid_piece: ChessPiece):
|
||||
valid_piece.player = Player.BLACK
|
||||
assert valid_piece.player == Player.BLACK
|
||||
|
||||
valid_piece.player = Player.WHITE
|
||||
assert valid_piece.player == Player.WHITE
|
||||
|
||||
def test_repr_str(valid_piece: ChessPiece):
|
||||
rep = str(valid_piece)
|
||||
assert 'player='
|
||||
|
||||
# move set testing (kinda separate from main project)
|
||||
|
||||
_init_val = 4
|
||||
|
||||
# static move sets
|
||||
|
||||
_static_move_sets = [(1, 0), (-3, 0), (0, 2), (0, -1), (4, 4), (-2, -2), (1, -3), (-3, 4)]
|
||||
|
||||
@fixture
|
||||
def valid_static_move_set():
|
||||
return StaticMoveSet(*_static_move_sets)
|
||||
|
||||
# test valid
|
||||
|
||||
def test_valid_static_moves(valid_static_move_set: StaticMoveSet):
|
||||
for ms in _static_move_sets:
|
||||
assert valid_static_move_set.is_valid_move(Move(_init_val, _init_val, _init_val+ms[0], _init_val+ms[1]))
|
||||
|
||||
# test invalid
|
||||
|
||||
def test_invalid_static_moves(valid_static_move_set: StaticMoveSet):
|
||||
for ms in _static_move_sets:
|
||||
assert not valid_static_move_set.is_valid_move(Move(_init_val, _init_val, _init_val+ms[0]+(-1 if ms[0] < 0 else 1), _init_val+ms[1]+(-1 if ms[1] < 0 else 1)))
|
||||
|
||||
# dynamic move sets
|
||||
|
||||
_dynamic_move_sets = [(4, 0), (-2, 0), (0, 8), (0, -6), (4, 4), (-4, -4), (2, -5), (-3, 4)]
|
||||
|
||||
@fixture
|
||||
def valid_dynamic_move_set():
|
||||
return DynamicMoveSet(*_dynamic_move_sets)
|
||||
|
||||
def test_valid_dynamic_moves(valid_dynamic_move_set: DynamicMoveSet):
|
||||
for ms in _dynamic_move_sets:
|
||||
row = ms[0]
|
||||
col = ms[1]
|
||||
|
||||
# find a valid range on numbers to select from using the row and column
|
||||
valid_range_row = valid_range(row)
|
||||
valid_range_col = valid_range(col)
|
||||
|
||||
# check if the ranges for rows and columns are empty individually, if so;
|
||||
# set random value to 0, if not, set it to a random element from it's respective list
|
||||
if len(valid_range_row) == 0:
|
||||
rnd_row = 0
|
||||
else:
|
||||
rnd_row = choice(valid_range_row)
|
||||
|
||||
if len(valid_range_col) == 0:
|
||||
rnd_col = 0
|
||||
else:
|
||||
rnd_col = choice(valid_range_col)
|
||||
|
||||
# test dat thing
|
||||
assert valid_dynamic_move_set.is_valid_move(Move(_init_val, _init_val, _init_val+rnd_row, _init_val+rnd_col))
|
||||
|
||||
_RND_MIN = 10
|
||||
_RND_MAX = 20
|
||||
|
||||
def test_invalid_dynamic_moves(valid_dynamic_move_set: DynamicMoveSet):
|
||||
for ms in _dynamic_move_sets:
|
||||
row = ms[0]
|
||||
col = ms[1]
|
||||
|
||||
# check if the row and column ranges are equal to zero, if so;
|
||||
# set random value to 0, if not, create random number between _rnd_min and _rnd_max
|
||||
# then, add the random number, making it negative if the column or row in that instance > 0
|
||||
if row == 0:
|
||||
row_rnd_add = 0
|
||||
else:
|
||||
rnd = randint(_RND_MIN, _RND_MAX)
|
||||
row_rnd_add = row + (rnd if row > 0 else -rnd)
|
||||
|
||||
if col == 0:
|
||||
col_rnd_add = 0
|
||||
else:
|
||||
rnd = randint(_RND_MIN, _RND_MAX)
|
||||
col_rnd_add = col + (rnd if col > 0 else -rnd)
|
||||
|
||||
#print(f'{ms}, ({_init_val}+{row_rnd_add}, {_init_val}+{col_rnd_add}) = ({_init_val+row_rnd_add}, {_init_val+col_rnd_add})')
|
||||
assert not valid_dynamic_move_set.is_valid_move(Move(_init_val, _init_val, _init_val+row_rnd_add, _init_val+col_rnd_add))
|
||||
Reference in New Issue
Block a user