diff --git a/abc.py b/abc.py new file mode 100644 index 0000000..eade1a7 --- /dev/null +++ b/abc.py @@ -0,0 +1,28 @@ +def split_and_join(line): + full = [] + current = '' + line_len = len(line) + for c in range(line_len): + char = line[c] + if char == ' ': + full.append(current) + current = '' + else: + current = current + char + + if c == line_len - 1: + full.append(str(current)) + + full_final = '' + full_len = len(full) + for v in range(full_len): + val = full[v] + full_final += val + ('' if v == full_len-1 else '-') + + return full_final + +if __name__ == '__main__': + line = input() + result = split_and_join(line) + print(result) + diff --git a/book_stuff/student.py b/book_stuff/student.py new file mode 100644 index 0000000..f6c32d6 --- /dev/null +++ b/book_stuff/student.py @@ -0,0 +1,43 @@ +class Student: + def __init__(self, name: str, gNumber: int, gpa: float): + self.set_name(name) + self.set_gNumber(gNumber) + self.set_gpa(gpa) + + # --- Name --- + def get_name(self) -> str: + return self._name + + def set_name(self, value: str) -> None: + if not isinstance(value, str): + raise TypeError("Name must be a string") + if not value.isalpha(): + raise ValueError("Name must contain only alphabetic characters") + if not (1 <= len(value) <= 255): + raise ValueError("Name must be between 1 and 255 characters") + self._name = value + + # --- gNumber --- + def get_gNumber(self) -> int: + return self._gNumber + + def set_gNumber(self, value: int) -> None: + if not isinstance(value, int): + raise TypeError("gNumber must be an integer") + if not (100000 <= value <= 999999): + raise ValueError("gNumber must be a 6-digit number between 100000 and 999999") + self._gNumber = value + + # --- GPA --- + def get_gpa(self) -> float: + return self._gpa + + def set_gpa(self, value: float) -> None: + if not isinstance(value, (int, float)): + raise TypeError("GPA must be a number") + if not (0.0 <= value <= 6.0): + raise ValueError("GPA must be between 0.0 and 6.0") + self._gpa = float(value) + + def __str__(self) -> str: + return f"Student(Name: {self._name}, gNumber: {self._gNumber}, GPA: {self._gpa:.2f})" \ No newline at end of file diff --git a/book_stuff/test_student.py b/book_stuff/test_student.py new file mode 100644 index 0000000..cd310b1 --- /dev/null +++ b/book_stuff/test_student.py @@ -0,0 +1,90 @@ +import pytest +from student import Student # assumes Student class is in student.py + + +# ---------- FIXTURES ---------- +@pytest.fixture +def valid_student(): + """Fixture for a valid student object""" + return Student("Alice", 123456, 4.0) + +def test_create_valid_student(valid_student): + assert valid_student.get_name() == "Alice" + assert valid_student.get_gNumber() == 123456 + assert valid_student.get_gpa() == 4.0 + + +def test_update_name(valid_student): + valid_student.set_name("Bob") + assert valid_student.get_name() == "Bob" + + +def test_update_gNumber(valid_student): + valid_student.set_gNumber(654321) + assert valid_student.get_gNumber() == 654321 + + +def test_update_gpa(valid_student): + valid_student.set_gpa(5.5) + assert valid_student.get_gpa() == 5.5 + + +# ---------- NAME VALIDATION ---------- +@pytest.mark.parametrize("invalid_name", [ + "", # empty string + "A" * 256, # too long + "Alice123", # contains digits + "Alice!", # contains special character + 123, # not a string +]) +def test_invalid_name_raises(invalid_name): + with pytest.raises((ValueError, TypeError)): + Student(invalid_name, 123456, 3.0) + + +# ---------- gNumber VALIDATION ---------- +@pytest.mark.parametrize("invalid_gNumber", [ + 99999, # too small + 1000000, # too large + "123456", # not an int + 12.34, # float +]) +def test_invalid_gNumber_raises(invalid_gNumber): + with pytest.raises((ValueError, TypeError)): + Student("Charlie", invalid_gNumber, 3.5) + + +def test_boundary_gNumber_valid(): + s1 = Student("David", 100000, 2.0) + s2 = Student("Eve", 999999, 3.0) + assert s1.get_gNumber() == 100000 + assert s2.get_gNumber() == 999999 + + +# ---------- GPA VALIDATION ---------- +@pytest.mark.parametrize("invalid_gpa", [ + -0.1, # below range + 6.1, # above range + "4.0", # string instead of number + None, # NoneType +]) +def test_invalid_gpa_raises(invalid_gpa): + with pytest.raises((ValueError, TypeError)): + Student("Frank", 222222, invalid_gpa) + + +def test_boundary_gpa_valid(): + s1 = Student("Grace", 333333, 0.0) + s2 = Student("Heidi", 444444, 6.0) + assert s1.get_gpa() == 0.0 + assert s2.get_gpa() == 6.0 + + +# ---------- STRING REPRESENTATION ---------- +def test_str_representation(valid_student): + s = str(valid_student) + assert "Alice" in s + assert "123456" in s + assert "4.00" in s + + diff --git a/chess/chess_gui_small_view.py b/chess/chess_gui_small_view.py new file mode 100644 index 0000000..c43c08e --- /dev/null +++ b/chess/chess_gui_small_view.py @@ -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('Laker Chess

White moves first.
', + 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 + '
') + 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 + '
') + + else: + self._side_box.append_html_text(f'{self.__model.messageCode}
') + 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!
GAME OVER!') + else: + self._side_box.append_html_text(f'{player_color} is in CHECK!
') + + 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...
") + if event.ui_element == self._undo_button: + try: + self.__model.undo() + self._side_box.append_html_text('Undoing move.
') + except UndoException as e: + self._side_box.append_html_text(f'{e}
') + 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() diff --git a/chess/chess_model.py b/chess/chess_model.py new file mode 100644 index 0000000..104c6b7 --- /dev/null +++ b/chess/chess_model.py @@ -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 \ No newline at end of file diff --git a/chess/chess_piece.py b/chess/chess_piece.py new file mode 100644 index 0000000..5631238 --- /dev/null +++ b/chess/chess_piece.py @@ -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 \ No newline at end of file diff --git a/chess/images/pieces.png b/chess/images/pieces.png new file mode 100644 index 0000000..5e838d2 Binary files /dev/null and b/chess/images/pieces.png differ diff --git a/chess/images/small_pieces.png b/chess/images/small_pieces.png new file mode 100644 index 0000000..e060ad5 Binary files /dev/null and b/chess/images/small_pieces.png differ diff --git a/chess/move.py b/chess/move.py new file mode 100644 index 0000000..bd34a9c --- /dev/null +++ b/chess/move.py @@ -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 \ No newline at end of file diff --git a/chess/pawn.py b/chess/pawn.py new file mode 100644 index 0000000..259aea7 --- /dev/null +++ b/chess/pawn.py @@ -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) \ No newline at end of file diff --git a/chess/player.py b/chess/player.py new file mode 100644 index 0000000..2de1360 --- /dev/null +++ b/chess/player.py @@ -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] + diff --git a/chess/test.py b/chess/test.py new file mode 100644 index 0000000..82559e0 --- /dev/null +++ b/chess/test.py @@ -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)) \ No newline at end of file