more cool stuff

chess, woah
This commit is contained in:
cutsweettea
2025-10-15 12:46:55 -04:00
parent 52ee1d7a8b
commit cb8e6e9b5a
12 changed files with 663 additions and 0 deletions

View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

121
chess/move.py Normal file
View 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
View 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
View 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
View 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))