From 8492b8b73390e1081f32c8a96d87b0a6539ee9d3 Mon Sep 17 00:00:00 2001 From: ca20110820 Date: Wed, 15 May 2024 16:25:21 +0800 Subject: [PATCH 01/18] initialized project --- projects/computer-algebra/README.md | 3 +++ projects/computer-algebra/main.py | 0 projects/computer-algebra/requirements.txt | 2 ++ 3 files changed, 5 insertions(+) create mode 100644 projects/computer-algebra/README.md create mode 100644 projects/computer-algebra/main.py create mode 100644 projects/computer-algebra/requirements.txt diff --git a/projects/computer-algebra/README.md b/projects/computer-algebra/README.md new file mode 100644 index 00000000..14d0f3af --- /dev/null +++ b/projects/computer-algebra/README.md @@ -0,0 +1,3 @@ +# Computer Algebra + + diff --git a/projects/computer-algebra/main.py b/projects/computer-algebra/main.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/computer-algebra/requirements.txt b/projects/computer-algebra/requirements.txt new file mode 100644 index 00000000..42e60772 --- /dev/null +++ b/projects/computer-algebra/requirements.txt @@ -0,0 +1,2 @@ +requests==2.31.0 +beautifulsoup4==4.12.3 \ No newline at end of file From 27f442202429a66794a948e7402090a46dc9e8bc Mon Sep 17 00:00:00 2001 From: ca20110820 Date: Wed, 15 May 2024 16:27:34 +0800 Subject: [PATCH 02/18] feat: added command module for the base command --- projects/computer-algebra/command.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 projects/computer-algebra/command.py diff --git a/projects/computer-algebra/command.py b/projects/computer-algebra/command.py new file mode 100644 index 00000000..d621739b --- /dev/null +++ b/projects/computer-algebra/command.py @@ -0,0 +1,24 @@ +from typing import Any +from abc import abstractmethod, ABC +from urllib.parse import quote + + +class CmdBase(ABC): + """Base class for all the CAS (Computer Algebra System) API Commands.""" + + def __init__(self, operation: str, base_url: str): + self.operation = operation + self.base_url = base_url + + @abstractmethod + def command(self, expr: str) -> Any: + """ + Command for sending request to Newton CAS API with a given expression string and returns the result from + the API response. + """ + pass + + @staticmethod + def url_encode(inp_str: str) -> str: + """Encode the input string to a URL-safe format.""" + return quote(inp_str) From cd4247933c9d0ab6c78eadd11b5c6da077461794 Mon Sep 17 00:00:00 2001 From: ca20110820 Date: Wed, 15 May 2024 16:33:27 +0800 Subject: [PATCH 03/18] feat: added concrete implementations of command for newton api --- projects/computer-algebra/newton_command.py | 76 +++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 projects/computer-algebra/newton_command.py diff --git a/projects/computer-algebra/newton_command.py b/projects/computer-algebra/newton_command.py new file mode 100644 index 00000000..794e4f39 --- /dev/null +++ b/projects/computer-algebra/newton_command.py @@ -0,0 +1,76 @@ +from dataclasses import dataclass + +import requests +from command import CmdBase + + +@dataclass(frozen=True) +class NewtonResponse: + """Newton API Response.""" + operation: str + expression: str + result: str + + +class NewtonCmdException(Exception): + """Base class for Newton Command Exceptions.""" + + +class NewtonCommand(CmdBase): + """Base class for all the Newton API Commands.""" + + def __init__(self, operation: str): + super().__init__(operation, 'https://newton.now.sh/api/v2') + + def command(self, expr: str) -> NewtonResponse: + """ + Command method for NewtonCommand class. + + Args: + expr (str): Mathematical expression to be evaluated. + + Returns: + NewtonResponse: Object containing the operation, expression, and result of the evaluated expression. + + Raises: + NewtonCmdException: If the HTTP request fails or returns a non-success status code, the exception is raised + with the error message. + """ + # Construct the Request URL + expr_encode = self.url_encode(expr) # URL Encode for Expression + request_url = f"{self.base_url}/{self.operation}/{expr_encode}" + + # Make the HTTP GET request + response = requests.get(request_url) + + # Check if the request was successful (status code 200) + if response.status_code == 200: + # Deserialize the JSON response into a dictionary + response_data = response.json() + + # Extract relevant data from the response + operation = response_data['operation'] + expression = response_data['expression'] + result = response_data['result'] + + # Create and return a NewtonResponse object + return NewtonResponse(operation=operation, expression=expression, result=result) + else: + raise NewtonCmdException(f'{response.text}') + + +newton_simplify = NewtonCommand('simplify').command +newton_factor = NewtonCommand('factor').command +newton_derive = NewtonCommand('derive').command +newton_integrate = NewtonCommand('integrate').command +newton_zeroes = NewtonCommand('zeroes').command +newton_tangent = NewtonCommand('tangent').command +newton_area = NewtonCommand('area').command +newton_cos = NewtonCommand('cos').command +newton_sin = NewtonCommand('sin').command +newton_tan = NewtonCommand('tan').command +newton_arc_cos = NewtonCommand('arccos').command +newton_arc_sin = NewtonCommand('arcsin').command +newton_arc_tan = NewtonCommand('arctan').command +newton_abs = NewtonCommand('abs').command +newton_log = NewtonCommand('log').command From d76bc8aac07742ffeed51fe6d70390d24ed56a3c Mon Sep 17 00:00:00 2001 From: ca20110820 Date: Wed, 15 May 2024 17:43:47 +0800 Subject: [PATCH 04/18] feat: added gui for the computer algebra app using dearpygui --- projects/computer-algebra/main.py | 109 ++++++++++++++++++++ projects/computer-algebra/newton_command.py | 18 ++++ projects/computer-algebra/requirements.txt | 2 +- 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/projects/computer-algebra/main.py b/projects/computer-algebra/main.py index e69de29b..1f7ab27e 100644 --- a/projects/computer-algebra/main.py +++ b/projects/computer-algebra/main.py @@ -0,0 +1,109 @@ +from newton_command import NEWTON_CMDS_DICT + +import dearpygui.dearpygui as dpg + + +def selection_cb(sender, app_data, user_data): + if user_data[1]: + print("User selected 'Ok'") + else: + print("User selected 'Cancel'") + + # delete window + dpg.delete_item(user_data[0]) + + +def show_info(title, message, selection_callback): + # Reference: https://github.com/hoffstadt/DearPyGui/discussions/1002 + + # guarantee these commands happen in the same frame + with dpg.mutex(): + viewport_width = dpg.get_viewport_client_width() + viewport_height = dpg.get_viewport_client_height() + + with dpg.window(tag='popup-window', label=title, modal=True, no_close=True) as modal_id: + dpg.add_text(message) + dpg.add_button(label="Ok", width=75, user_data=(modal_id, True), callback=selection_callback) + dpg.add_same_line() + dpg.add_button(label="Cancel", width=75, user_data=(modal_id, False), callback=selection_callback) + + # guarantee these commands happen in another frame + dpg.split_frame() + width = dpg.get_item_width(modal_id) + height = dpg.get_item_height(modal_id) + dpg.set_item_pos(modal_id, [viewport_width // 2 - width // 2, viewport_height // 2 - height // 2]) + + +# Callbacks and Helpers +def on_evaluate(sender, app_data, user_data): + # Get the Command + cmd = dpg.get_value('radio-cmds') + cmd_func = NEWTON_CMDS_DICT[cmd] + + # Get the Expression + expr = dpg.get_value('inp-expr') + + if expr.strip() in ['']: + show_info( + 'Error', + 'Please use valid mathematical expressions.', + selection_cb + ) + # Clear Expression + dpg.set_value('inp-expr', '') + return + + # Evaluate + response = cmd_func(expr) + result = response.result + + dpg.set_value('label-output', result) + + +dpg.create_context() +dpg.create_viewport(title='Computer Algebra', width=1300, height=750) + +with dpg.window(tag='inp-window', + label="Input", + pos=[0, 0], + autosize=True, + # width=1150, + # height=350, + no_collapse=True, + no_close=True, + ): + # Radio Button for Commands + dpg.add_radio_button( + horizontal=True, + tag='radio-cmds', + items=[cmd for cmd in NEWTON_CMDS_DICT.keys()] + ) + + # Text Area for Mathematical Expression + dpg.add_input_text( + tag='inp-expr', + width=int(1150 * 0.8), + ) + + # Button for Evaluating Command and Expression + dpg.add_button(label="Evaluate", callback=on_evaluate) + +with dpg.window(tag='out-window', + pos=[0, 100], + label="Output", + # width=700, + # height=350, + autosize=True, + no_collapse=True, + no_close=True, + ): + # Use Label for Output + dpg.add_text(tag='label-output', + label='Result', + show_label=True, + ) + +dpg.setup_dearpygui() +dpg.show_viewport() +dpg.start_dearpygui() +dpg.destroy_context() diff --git a/projects/computer-algebra/newton_command.py b/projects/computer-algebra/newton_command.py index 794e4f39..e38e1e88 100644 --- a/projects/computer-algebra/newton_command.py +++ b/projects/computer-algebra/newton_command.py @@ -74,3 +74,21 @@ def command(self, expr: str) -> NewtonResponse: newton_arc_tan = NewtonCommand('arctan').command newton_abs = NewtonCommand('abs').command newton_log = NewtonCommand('log').command + +NEWTON_CMDS_DICT = { + 'simplify': newton_simplify, + 'factor': newton_factor, + 'derive': newton_derive, + 'integrate': newton_integrate, + 'zeroes': newton_zeroes, + 'tangent': newton_tangent, + 'area': newton_area, + 'cos': newton_cos, + 'sin': newton_sin, + 'tan': newton_tan, + 'arccos': newton_arc_cos, + 'arcsin': newton_arc_sin, + 'arctan': newton_arc_tan, + 'abs': newton_abs, + 'log': newton_log +} diff --git a/projects/computer-algebra/requirements.txt b/projects/computer-algebra/requirements.txt index 42e60772..d9e5829e 100644 --- a/projects/computer-algebra/requirements.txt +++ b/projects/computer-algebra/requirements.txt @@ -1,2 +1,2 @@ requests==2.31.0 -beautifulsoup4==4.12.3 \ No newline at end of file +dearpygui==1.11.1 \ No newline at end of file From cf5406ad4356f9ecac76df19447d59bfd02e740e Mon Sep 17 00:00:00 2001 From: ca20110820 Date: Wed, 15 May 2024 17:48:10 +0800 Subject: [PATCH 05/18] docs: added docstrings in main entrypoint --- projects/computer-algebra/main.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/projects/computer-algebra/main.py b/projects/computer-algebra/main.py index 1f7ab27e..dc6c36d4 100644 --- a/projects/computer-algebra/main.py +++ b/projects/computer-algebra/main.py @@ -4,6 +4,7 @@ def selection_cb(sender, app_data, user_data): + """Callback function for button selections in the message box.""" if user_data[1]: print("User selected 'Ok'") else: @@ -14,7 +15,12 @@ def selection_cb(sender, app_data, user_data): def show_info(title, message, selection_callback): - # Reference: https://github.com/hoffstadt/DearPyGui/discussions/1002 + """ + Display an information message box with title, message, and callback. + + References: + https://github.com/hoffstadt/DearPyGui/discussions/1002 + """ # guarantee these commands happen in the same frame with dpg.mutex(): @@ -36,6 +42,7 @@ def show_info(title, message, selection_callback): # Callbacks and Helpers def on_evaluate(sender, app_data, user_data): + """Callback function for the 'Evaluate' button.""" # Get the Command cmd = dpg.get_value('radio-cmds') cmd_func = NEWTON_CMDS_DICT[cmd] From a5a38903ca4eb1b6184d1812f5ee9862b7b0123b Mon Sep 17 00:00:00 2001 From: ca20110820 Date: Wed, 15 May 2024 18:04:30 +0800 Subject: [PATCH 06/18] docs: created README for introducing the project --- projects/computer-algebra/README.md | 55 ++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/projects/computer-algebra/README.md b/projects/computer-algebra/README.md index 14d0f3af..5d2af9df 100644 --- a/projects/computer-algebra/README.md +++ b/projects/computer-algebra/README.md @@ -1,3 +1,56 @@ -# Computer Algebra +# Newton CAS Python Wrapper and GUI +This project aims to provide a Python wrapper and GUI for the Newton API, a Computer Algebra +System (CAS) that allows users to perform various mathematical computations. The GUI is built using +[DearPyGui](https://github.com/hoffstadt/DearPyGui) and +[Newton API](https://github.com/aunyks/newton-api). +## Features + +- **User-Friendly Interface:** The GUI provides an intuitive interface for users to interact with the Newton API + effortlessly. +- **Multiple Mathematical Operations:** Users can perform a variety of mathematical operations such as simplification, + factoring, differentiation, integration, finding zeroes, and more. +- **Real-Time Evaluation:** Expressions are evaluated in real-time, providing instant feedback to users. + +## Installation + +1. Clone the repository: + + ```bash + git clone https://github.com/Mrinank-Bhowmick/python-beginner-projects.git + ``` + +2. Navigate to the project directory: + + ```bash + cd python-beginner-projects/projects/computer-algebra + ``` + +3. Install dependencies using pip: + + ```bash + pip install -r requirements.txt + ``` + +## Usage + +1. Run the main script `main.py`: + + ```bash + python main.py + ``` + +2. The application window will appear, consisting of two sections: + - **Input Section:** Enter the mathematical expression you want to evaluate. + - **Output Section:** View the result of the evaluation. + +3. Choose the desired mathematical operation from the radio buttons. +4. Enter the expression in the input text box. + - See valid syntax from [Newton API](https://github.com/aunyks/newton-api). +5. Click the "Evaluate" button to perform the selected operation. +6. The result will be displayed in the output section. + +## Contact + +[GitHub Profile](https://github.com/ca20110820) From d46ba1546f7bd7fd0d983df4f1b0eb185e778249 Mon Sep 17 00:00:00 2001 From: Amer Sbahi Date: Tue, 21 May 2024 20:55:36 +0300 Subject: [PATCH 07/18] First, made a database to store scores with their names. Second, modified the first conditional in the game to handle if the player isn't ready --- projects/Quiz Game/main.py | 75 +++++++++++++++++++++++++++++----- projects/Quiz Game/setup_db.py | 27 ++++++++++++ 2 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 projects/Quiz Game/setup_db.py diff --git a/projects/Quiz Game/main.py b/projects/Quiz Game/main.py index aab901b5..8c9d4bb3 100644 --- a/projects/Quiz Game/main.py +++ b/projects/Quiz Game/main.py @@ -1,3 +1,37 @@ +# import the sqlite3 to reterive previous scores +import sqlite3 +# create Conn to have the database file +def create_connection(db_file): + """ create a database connection to the SQLite database """ + conn = None + try: + conn = sqlite3.connect(db_file) + except sqlite3.Error as e: + print(e) + return conn + # store the player's score with their name +def save_score(conn, name, score): + """ + Save the player's score to the database + """ + sql = ''' INSERT INTO scores(name, score) + VALUES(?,?) ''' + cur = conn.cursor() + cur.execute(sql, (name, score)) + conn.commit() + return cur.lastrowid +# recall the previous scores to display them +def get_all_scores(conn): + """ + Query all rows in the scores table + """ + cur = conn.cursor() + cur.execute("SELECT * FROM scores ORDER BY score DESC") + + rows = cur.fetchall() + return rows + +# The beginning of the game print("Welcome to AskPython Quiz") # Get user's readiness to play the Quiz @@ -26,23 +60,42 @@ print("Wrong Answer :(") # User's answer is incorrect # Question 3 - answer = input( - "Question 3: What is the name of your favourite website for learning Python?" - ) + answer = input("Question 3: What is the name of your favourite website for learning Python?") if answer.lower() == "askpython": score += 1 print("correct") # User's answer is correct, increment the score else: print("Wrong Answer :(") # User's answer is incorrect -# Display the result and user's score -print( - "Thank you for Playing this small quiz game, you attempted", - score, - "questions correctly!", -) -mark = int((score / total_questions) * 100) -print(f"Marks obtained: {mark}%") + # Display the result and user's score + print("Thank you for Playing this small quiz game, you attempted", score, "questions correctly!") + mark = int((score / total_questions) * 100) + print(f"Marks obtained: {mark}%") + + # Getting the player's name and score to insert into the database + player_name = input("Enter your name: ") + player_score = score + + database = "quiz_game.db" + + # Create a database connection + conn = create_connection(database) + + if conn is not None: + # Save the player's score + save_score(conn, player_name, player_score) + + # Display all scores + print("Previous scores:") + scores = get_all_scores(conn) + for row in scores: + print(f"Name: {row[1]}, Score: {row[2]}, Date: {row[3]}") + + conn.close() + else: + print("Error! Cannot create the database connection.") +else: + print(" Please, when you're ready, enter the game again.") # Farewell message print("BYE!") diff --git a/projects/Quiz Game/setup_db.py b/projects/Quiz Game/setup_db.py new file mode 100644 index 00000000..cb3b7c34 --- /dev/null +++ b/projects/Quiz Game/setup_db.py @@ -0,0 +1,27 @@ +import sqlite3 + +def create_database(): + # Connect to the SQLite database (it will create the file if it doesn't exist) + conn = sqlite3.connect('quiz_game.db') + + # Create a cursor object + cursor = conn.cursor() + + # Create a table to store players' scores + cursor.execute(''' + CREATE TABLE IF NOT EXISTS scores ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + score INTEGER NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Commit the changes and close the connection + conn.commit() + conn.close() + + print("Database and table created successfully.") + +if __name__ == "__main__": + create_database() From 7ca1dac0f6b3ef8fd70e34ad37576544af06d990 Mon Sep 17 00:00:00 2001 From: ca20110820 Date: Wed, 22 May 2024 13:17:15 +0800 Subject: [PATCH 08/18] docs: updated computer-algebra license --- projects/computer-algebra/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/projects/computer-algebra/README.md b/projects/computer-algebra/README.md index 5d2af9df..14c95d5c 100644 --- a/projects/computer-algebra/README.md +++ b/projects/computer-algebra/README.md @@ -54,3 +54,7 @@ System (CAS) that allows users to perform various mathematical computations. The ## Contact [GitHub Profile](https://github.com/ca20110820) + +## License + +[![License](https://img.shields.io/static/v1?label=Licence&message=GPL-3-0&color=blue)](https://opensource.org/license/GPL-3-0) From a082507b1c9853ed72288319629cd00b60c719f7 Mon Sep 17 00:00:00 2001 From: Lisa Nguyen Date: Sun, 2 Jun 2024 21:47:00 +0800 Subject: [PATCH 09/18] refactor original code into separate modules --- projects/Snake Game/constants.py | 27 +++++ projects/Snake Game/display.py | 38 ++++++ projects/Snake Game/game.py | 197 +++++++++---------------------- projects/Snake Game/main.py | 5 + projects/Snake Game/snake.py | 29 +++++ 5 files changed, 157 insertions(+), 139 deletions(-) create mode 100644 projects/Snake Game/constants.py create mode 100644 projects/Snake Game/display.py create mode 100644 projects/Snake Game/main.py create mode 100644 projects/Snake Game/snake.py diff --git a/projects/Snake Game/constants.py b/projects/Snake Game/constants.py new file mode 100644 index 00000000..6cfb64e7 --- /dev/null +++ b/projects/Snake Game/constants.py @@ -0,0 +1,27 @@ +from enum import Enum +from collections import namedtuple + + +class RgbColors: + WHITE = (255, 255, 255) + RED = (200, 0, 0) + BLUE1 = (0, 0, 255) + BLUE2 = (0, 100, 255) + BLACK = (0, 0, 0) + + +class GameSettings: + BLOCK_SIZE = 20 + SPEED = 5 + WIDTH = 640 + HEIGHT = 480 + + +class Direction(Enum): + RIGHT = 1 + LEFT = 2 + UP = 3 + DOWN = 4 + + +Point = namedtuple('Point', ['x', 'y']) diff --git a/projects/Snake Game/display.py b/projects/Snake Game/display.py new file mode 100644 index 00000000..9faf1591 --- /dev/null +++ b/projects/Snake Game/display.py @@ -0,0 +1,38 @@ +import pygame +from constants import RgbColors, GameSettings + + +class Display: + def __init__(self): + pygame.init() + self.width = GameSettings.WIDTH + self.height = GameSettings.HEIGHT + self.font = pygame.font.Font(None, 25) + self.window = pygame.display.set_mode((self.width, self.height)) + pygame.display.set_caption("Snake") + self.clock = pygame.time.Clock() + + def update_ui(self, snake, food, score): + self.window.fill(RgbColors.BLACK) + # Draw snake + for block in snake.blocks: + pygame.draw.rect( + self.window, RgbColors.BLUE1, + pygame.Rect(block.x, block.y, GameSettings.BLOCK_SIZE, GameSettings.BLOCK_SIZE) + ) + pygame.draw.rect( + self.window, RgbColors.BLUE2, pygame.Rect(block.x + 4, block.y + 4, 12, 12) + ) + # Draw food + pygame.draw.rect( + self.window, + RgbColors.RED, + pygame.Rect(food.x, food.y, GameSettings.BLOCK_SIZE, GameSettings.BLOCK_SIZE), + ) + # Draw score + score_display = self.font.render(f"Score: {score}", True, RgbColors.WHITE) + self.window.blit(score_display, [0, 0]) # score in top left corner of window + pygame.display.flip() + + def clock_tick(self, speed): + self.clock.tick(speed) diff --git a/projects/Snake Game/game.py b/projects/Snake Game/game.py index 8101a9c9..2818a9f9 100644 --- a/projects/Snake Game/game.py +++ b/projects/Snake Game/game.py @@ -1,162 +1,81 @@ import pygame import random -from enum import Enum -from collections import namedtuple +from snake import Snake, Direction, Point +from display import Display +from constants import GameSettings -pygame.init() -font = pygame.font.Font(None, 25) - - -class Direction(Enum): - RIGHT = 1 - LEFT = 2 - UP = 3 - DOWN = 4 - - -Point = namedtuple("Point", "x, y") - -# rgb colors -WHITE = (255, 255, 255) -RED = (200, 0, 0) -BLUE1 = (0, 0, 255) -BLUE2 = (0, 100, 255) -BLACK = (0, 0, 0) - -BLOCK_SIZE = 20 -SPEED = 5 - - -class SnakeGame: - def __init__(self, w=640, h=480): - self.w = w - self.h = h - # init display - self.display = pygame.display.set_mode((self.w, self.h)) - pygame.display.set_caption("Snake") - self.clock = pygame.time.Clock() - - # init game state - self.direction = Direction.RIGHT - - self.head = Point(self.w / 2, self.h / 2) - self.snake = [ - self.head, - Point(self.head.x - BLOCK_SIZE, self.head.y), - Point(self.head.x - (2 * BLOCK_SIZE), self.head.y), - ] +class Game: + def __init__(self): + self.display = Display() + self.snake = Snake() self.score = 0 self.food = None - self._place_food() + self.place_food() + + def game_loop(self): + while True: + self.play_step() + game_over, score = self.play_step() + if game_over: + break + print("Final Score:", self.score) + pygame.quit() + + def is_collision(self): + # Snake hits boundary + if ( + self.snake.head.x > self.display.width - self.snake.block_size + or self.snake.head.x < 0 + or self.snake.head.y > self.display.height - self.snake.block_size + or self.snake.head.y < 0 + ): + return True + # Snake hits itself + if self.snake.self_collision(): + return True + return False - def _place_food(self): - x = random.randint(0, (self.w - BLOCK_SIZE) // BLOCK_SIZE) * BLOCK_SIZE - y = random.randint(0, (self.h - BLOCK_SIZE) // BLOCK_SIZE) * BLOCK_SIZE - self.food = Point(x, y) - if self.food in self.snake: - self._place_food() + def game_over(self): + return self.is_collision() - def play_step(self): - # 1. collect user input + def get_user_input(self): for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() quit() if event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT: - self.direction = Direction.LEFT + self.snake.direction = Direction.LEFT elif event.key == pygame.K_RIGHT: - self.direction = Direction.RIGHT + self.snake.direction = Direction.RIGHT elif event.key == pygame.K_UP: - self.direction = Direction.UP + self.snake.direction = Direction.UP elif event.key == pygame.K_DOWN: - self.direction = Direction.DOWN - - # 2. move - self._move(self.direction) # update the head - self.snake.insert(0, self.head) + self.snake.direction = Direction.DOWN - # 3. check if game over - game_over = False - if self._is_collision(): - game_over = True - return game_over, self.score - - # 4. place new food or just move - if self.head == self.food: + def play_step(self): + self.get_user_input() + self.snake.move(self.snake.direction) + if self.snake.head == self.food: self.score += 1 - self._place_food() + self.place_food() else: - self.snake.pop() - - # 5. update ui and clock - self._update_ui() - self.clock.tick(SPEED) - # 6. return game over and score + self.snake.blocks.pop() + # Update UI and Clock + self.display.update_ui(self.snake, self.food, self.score) + self.display.clock_tick(GameSettings.SPEED) + game_over = self.is_collision() return game_over, self.score - def _is_collision(self): - # hits boundary - if ( - self.head.x > self.w - BLOCK_SIZE - or self.head.x < 0 - or self.head.y > self.h - BLOCK_SIZE - or self.head.y < 0 - ): - return True - # hits itself - if self.head in self.snake[1:]: - return True - - return False - - def _update_ui(self): - self.display.fill(BLACK) - - for pt in self.snake: - pygame.draw.rect( - self.display, BLUE1, pygame.Rect(pt.x, pt.y, BLOCK_SIZE, BLOCK_SIZE) - ) - pygame.draw.rect( - self.display, BLUE2, pygame.Rect(pt.x + 4, pt.y + 4, 12, 12) - ) - - pygame.draw.rect( - self.display, - RED, - pygame.Rect(self.food.x, self.food.y, BLOCK_SIZE, BLOCK_SIZE), - ) - - text = font.render("Score: " + str(self.score), True, WHITE) - self.display.blit(text, [0, 0]) - pygame.display.flip() - - def _move(self, direction): - x = self.head.x - y = self.head.y - if direction == Direction.RIGHT: - x += BLOCK_SIZE - elif direction == Direction.LEFT: - x -= BLOCK_SIZE - elif direction == Direction.DOWN: - y += BLOCK_SIZE - elif direction == Direction.UP: - y -= BLOCK_SIZE - - self.head = Point(x, y) - - -if __name__ == "__main__": - game = SnakeGame() - - # game loop - while True: - game_over, score = game.play_step() - - if game_over == True: - break - - print("Final Score", score) + def place_food(self): + x = random.randint(0, ( + self.display.width - GameSettings.BLOCK_SIZE) // GameSettings.BLOCK_SIZE) * GameSettings.BLOCK_SIZE + y = random.randint(0, ( + self.display.height - GameSettings.BLOCK_SIZE) // GameSettings.BLOCK_SIZE) * GameSettings.BLOCK_SIZE + self.food = Point(x, y) + if self.food in self.snake.blocks: + self.place_food() - pygame.quit() + def get_score(self): + return self.score diff --git a/projects/Snake Game/main.py b/projects/Snake Game/main.py new file mode 100644 index 00000000..ec0706c7 --- /dev/null +++ b/projects/Snake Game/main.py @@ -0,0 +1,5 @@ +from game import Game + +if __name__ == "__main__": + game = Game() + game.game_loop() diff --git a/projects/Snake Game/snake.py b/projects/Snake Game/snake.py new file mode 100644 index 00000000..09ca36f8 --- /dev/null +++ b/projects/Snake Game/snake.py @@ -0,0 +1,29 @@ +from constants import GameSettings, Direction, Point + + +class Snake: + def __init__(self, init_length=3): + self.head = Point(GameSettings.WIDTH / 2, GameSettings.HEIGHT / 2) + self.block_size = GameSettings.BLOCK_SIZE + self.blocks = ([self.head] + + [Point(self.head.x - (i * self.block_size), self.head.y) for i in range(1, init_length)]) + self.direction = Direction.RIGHT + + def move(self, direction): + x, y = self.head + if direction == Direction.RIGHT: + x += self.block_size + elif direction == Direction.LEFT: + x -= self.block_size + elif direction == Direction.DOWN: + y += self.block_size + elif direction == Direction.UP: + y -= self.block_size + self.head = Point(x, y) + self.blocks.insert(0, self.head) + return self.head + + def self_collision(self): + if self.head in self.blocks[1:]: + return True + return False From c973a2cc5796986c1ef2a4659716bc6e9ed5dc6d Mon Sep 17 00:00:00 2001 From: Lisa Nguyen Date: Mon, 3 Jun 2024 22:34:25 +0800 Subject: [PATCH 10/18] add unit tests for game, display and snake --- projects/Snake Game/{ => src}/constants.py | 0 projects/Snake Game/{ => src}/display.py | 3 - projects/Snake Game/{ => src}/game.py | 2 +- projects/Snake Game/{ => src}/main.py | 0 projects/Snake Game/{ => src}/snake.py | 0 projects/Snake Game/tests/test_display.py | 78 ++++++++++++++++++++++ projects/Snake Game/tests/test_game.py | 53 +++++++++++++++ projects/Snake Game/tests/test_snake.py | 35 ++++++++++ 8 files changed, 167 insertions(+), 4 deletions(-) rename projects/Snake Game/{ => src}/constants.py (100%) rename projects/Snake Game/{ => src}/display.py (95%) rename projects/Snake Game/{ => src}/game.py (98%) rename projects/Snake Game/{ => src}/main.py (100%) rename projects/Snake Game/{ => src}/snake.py (100%) create mode 100644 projects/Snake Game/tests/test_display.py create mode 100644 projects/Snake Game/tests/test_game.py create mode 100644 projects/Snake Game/tests/test_snake.py diff --git a/projects/Snake Game/constants.py b/projects/Snake Game/src/constants.py similarity index 100% rename from projects/Snake Game/constants.py rename to projects/Snake Game/src/constants.py diff --git a/projects/Snake Game/display.py b/projects/Snake Game/src/display.py similarity index 95% rename from projects/Snake Game/display.py rename to projects/Snake Game/src/display.py index 9faf1591..60a680ea 100644 --- a/projects/Snake Game/display.py +++ b/projects/Snake Game/src/display.py @@ -33,6 +33,3 @@ def update_ui(self, snake, food, score): score_display = self.font.render(f"Score: {score}", True, RgbColors.WHITE) self.window.blit(score_display, [0, 0]) # score in top left corner of window pygame.display.flip() - - def clock_tick(self, speed): - self.clock.tick(speed) diff --git a/projects/Snake Game/game.py b/projects/Snake Game/src/game.py similarity index 98% rename from projects/Snake Game/game.py rename to projects/Snake Game/src/game.py index 2818a9f9..c61121d2 100644 --- a/projects/Snake Game/game.py +++ b/projects/Snake Game/src/game.py @@ -64,7 +64,7 @@ def play_step(self): self.snake.blocks.pop() # Update UI and Clock self.display.update_ui(self.snake, self.food, self.score) - self.display.clock_tick(GameSettings.SPEED) + self.display.clock.tick(GameSettings.SPEED) game_over = self.is_collision() return game_over, self.score diff --git a/projects/Snake Game/main.py b/projects/Snake Game/src/main.py similarity index 100% rename from projects/Snake Game/main.py rename to projects/Snake Game/src/main.py diff --git a/projects/Snake Game/snake.py b/projects/Snake Game/src/snake.py similarity index 100% rename from projects/Snake Game/snake.py rename to projects/Snake Game/src/snake.py diff --git a/projects/Snake Game/tests/test_display.py b/projects/Snake Game/tests/test_display.py new file mode 100644 index 00000000..335ac7ee --- /dev/null +++ b/projects/Snake Game/tests/test_display.py @@ -0,0 +1,78 @@ +import unittest +from unittest.mock import patch, MagicMock +import pygame +import pygame.time +from display import Display +from constants import RgbColors, GameSettings, Point +from snake import Snake + + +class TestDisplay(unittest.TestCase): + + def setUp(self): + self.display = Display() + self.snake = Snake() + + @patch('pygame.init') + @patch('pygame.font.Font') + @patch('pygame.display.set_mode') + @patch('pygame.display.set_caption') + @patch('pygame.time.Clock') + def test_init(self, mock_init, mock_font, mock_set_mode, mock_set_caption, mock_clock): + mock_init.return_value = True + mock_font_instance = MagicMock() + mock_font.return_value = mock_font_instance + mock_window = MagicMock() + mock_set_mode.return_value = mock_window + mock_clock_instance = MagicMock() + mock_clock.return_value = mock_clock_instance + + mock_set_mode.assert_called_with((GameSettings.WIDTH, GameSettings.HEIGHT)) + mock_set_caption.assert_called_with("Snake") + mock_clock.assert_called() + + self.assertEqual(self.display.width, GameSettings.WIDTH) + self.assertEqual(self.display.height, GameSettings.HEIGHT) + self.assertIsInstance(self.display.font, pygame.font.Font) + self.assertIsInstance(self.display.window, MagicMock) + self.assertIsInstance(self.display.clock, MagicMock) + + @patch('pygame.init') + @patch('pygame.display.set_mode') + @patch('pygame.display.set_caption') + @patch('pygame.font.Font') + @patch('pygame.time.Clock') + def test_init(self, mock_clock, mock_font, mock_set_caption, mock_set_mode, mock_init): + display = Display() + mock_init.assert_called_once() + mock_font.assert_called_once() + mock_set_caption.assert_called_with("Snake") + mock_set_mode.assert_called_with((GameSettings.WIDTH, GameSettings.HEIGHT)) + mock_clock.assert_called_once() + + self.assertIsInstance(display.font, MagicMock) + self.assertIsInstance(display.window, MagicMock) + self.assertIsInstance(display.clock, MagicMock) + + @patch('pygame.draw.rect') + @patch('pygame.font.Font') + @patch('pygame.display.flip') + @patch('pygame.display.set_mode') + def test_update_ui(self, mock_set_mode, mock_display_flip, mock_font, mock_draw_rect): + mock_window = mock_set_mode.return_value + mock_font_instance = mock_font.return_value + mock_font_instance.render = MagicMock() + + self.food = Point(100, 100) + self.score = 10 + self.display.window = mock_window + self.display.font = mock_font_instance + self.display.update_ui(self.snake, self.food, self.score) + + mock_draw_rect.assert_called() + mock_font_instance.render.assert_called() + mock_display_flip.assert_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/projects/Snake Game/tests/test_game.py b/projects/Snake Game/tests/test_game.py new file mode 100644 index 00000000..6d9eed82 --- /dev/null +++ b/projects/Snake Game/tests/test_game.py @@ -0,0 +1,53 @@ +import unittest +from unittest.mock import patch, MagicMock +from game import Game +from constants import GameSettings, Point + + +class TestGame(unittest.TestCase): + def setUp(self): + self.game = Game() + + def test_init(self): + self.assertIsNotNone(self.game.display) + self.assertIsNotNone(self.game.snake) + self.assertEqual(self.game.score, 0) + self.assertIsNotNone(self.game.food) + + def test_is_collision(self): + # Snake is out of bounds + self.game.snake.head = Point(-1, -1) + self.assertTrue(self.game.is_collision()) + # Snake collides with itself + self.game.snake.head = self.game.snake.blocks[1] + self.assertTrue(self.game.is_collision()) + + @patch('pygame.event.get') + @patch('pygame.draw.rect') + @patch('pygame.display.flip') + @patch('pygame.font.Font') + def test_play_step(self, mock_event_get, mock_draw_rect, mock_display_flip, mock_font): + mock_event_get.return_value = [] + mock_font_instance = MagicMock() + mock_font_instance.render.return_value = MagicMock() + mock_font.return_value = mock_font_instance + + init_snake_length = len(self.game.snake.blocks) + init_score = self.game.score + init_head_position = self.game.snake.head + # Place food in front of snake + self.game.food = Point(init_head_position.x + GameSettings.BLOCK_SIZE, init_head_position.y) + self.game.play_step() + + self.assertEqual(len(self.game.snake.blocks), init_snake_length + 1) + self.assertEqual(self.game.score, init_score + 1) + new_head_position = Point(init_head_position.x + GameSettings.BLOCK_SIZE, init_head_position.y) + self.assertEqual(self.game.snake.head, new_head_position) + + def test_place_food(self): + self.game.place_food() + self.assertNotIn(self.game.food, self.game.snake.blocks) + + +if __name__ == '__main__': + unittest.main() diff --git a/projects/Snake Game/tests/test_snake.py b/projects/Snake Game/tests/test_snake.py new file mode 100644 index 00000000..2de74486 --- /dev/null +++ b/projects/Snake Game/tests/test_snake.py @@ -0,0 +1,35 @@ +import unittest +from snake import Snake, Point, Direction +from constants import GameSettings + + +class TestSnake(unittest.TestCase): + def setUp(self): + self.snake = Snake() + + def test_init(self): + self.assertEqual(self.snake.head, Point(GameSettings.WIDTH / 2, GameSettings.HEIGHT / 2)) + self.assertEqual(self.snake.block_size, GameSettings.BLOCK_SIZE) + self.assertEqual(len(self.snake.blocks), 3) + self.assertEqual(self.snake.direction, Direction.RIGHT) + + def test_move(self): + init_head = self.snake.head + self.snake.move(Direction.RIGHT) + new_head_position = Point(init_head.x + GameSettings.BLOCK_SIZE, init_head.y) + self.assertEqual(self.snake.head, new_head_position) + self.assertEqual(self.snake.blocks[0], new_head_position) + + def test_self_collision(self): + self.snake.head = Point(100, 100) + self.snake.blocks = [ + self.snake.head, + Point(80, 100), + Point(60, 100), + Point(100, 100) + ] + self.assertTrue(self.snake.self_collision()) + + +if __name__ == '__main__': + unittest.main() From fe6666cae98384436cd7d0f64185a77255e83391 Mon Sep 17 00:00:00 2001 From: Lisa Nguyen Date: Mon, 3 Jun 2024 23:35:21 +0800 Subject: [PATCH 11/18] implement play again feature - resolves #743 --- projects/Snake Game/src/display.py | 32 ++++++++++++++++++++++++++---- projects/Snake Game/src/game.py | 23 ++++++++++++++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/projects/Snake Game/src/display.py b/projects/Snake Game/src/display.py index 60a680ea..327ceac4 100644 --- a/projects/Snake Game/src/display.py +++ b/projects/Snake Game/src/display.py @@ -14,7 +14,12 @@ def __init__(self): def update_ui(self, snake, food, score): self.window.fill(RgbColors.BLACK) - # Draw snake + self.draw_snake(snake) + self.draw_food(food) + self.draw_score(score) + pygame.display.flip() + + def draw_snake(self, snake): for block in snake.blocks: pygame.draw.rect( self.window, RgbColors.BLUE1, @@ -23,13 +28,32 @@ def update_ui(self, snake, food, score): pygame.draw.rect( self.window, RgbColors.BLUE2, pygame.Rect(block.x + 4, block.y + 4, 12, 12) ) - # Draw food + + def draw_food(self, food): pygame.draw.rect( self.window, RgbColors.RED, pygame.Rect(food.x, food.y, GameSettings.BLOCK_SIZE, GameSettings.BLOCK_SIZE), ) - # Draw score + + def draw_score(self, score): score_display = self.font.render(f"Score: {score}", True, RgbColors.WHITE) - self.window.blit(score_display, [0, 0]) # score in top left corner of window + self.window.blit(score_display, [0, 0]) # score in top left window corner + + def render_game_over(self): + self.font = pygame.font.Font(None, 48) + game_over_display = self.font.render("GAME OVER", True, RgbColors.WHITE) + text_width = game_over_display.get_width() + text_height = game_over_display.get_height() + text_x = (self.width - text_width) // 2 + text_y = (self.height // 4) - (text_height // 2) + self.window.blit(game_over_display, + [text_x, text_y]) + pygame.display.flip() + + def render_play_again(self): + self.font = pygame.font.Font(None, 32) + play_again_display = self.font.render("Play again? (Y/N)", True, RgbColors.WHITE) + display_box = play_again_display.get_rect(center=(self.width // 2, self.height // 2)) + self.window.blit(play_again_display, display_box) pygame.display.flip() diff --git a/projects/Snake Game/src/game.py b/projects/Snake Game/src/game.py index c61121d2..b08177a0 100644 --- a/projects/Snake Game/src/game.py +++ b/projects/Snake Game/src/game.py @@ -18,7 +18,11 @@ def game_loop(self): self.play_step() game_over, score = self.play_step() if game_over: - break + self.display.render_game_over() + self.display.render_play_again() + if not self.play_again(): + break + self.restart_game() print("Final Score:", self.score) pygame.quit() @@ -77,5 +81,18 @@ def place_food(self): if self.food in self.snake.blocks: self.place_food() - def get_score(self): - return self.score + def play_again(self): + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return False + if event.type == pygame.KEYDOWN: + if event.key in [pygame.K_n, pygame.K_ESCAPE]: + return False + if event.key in [pygame.K_y, pygame.K_RETURN]: + return True + + def restart_game(self): + self.snake = Snake() + self.score = 0 + self.place_food() \ No newline at end of file From 39e023bc6e3e1059e150926fdd3e24236376db47 Mon Sep 17 00:00:00 2001 From: Lisa Nguyen Date: Mon, 3 Jun 2024 23:50:47 +0800 Subject: [PATCH 12/18] add unit tests for play again feature --- projects/Snake Game/src/game.py | 2 +- projects/Snake Game/tests/test_game.py | 31 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/projects/Snake Game/src/game.py b/projects/Snake Game/src/game.py index b08177a0..b3d13940 100644 --- a/projects/Snake Game/src/game.py +++ b/projects/Snake Game/src/game.py @@ -95,4 +95,4 @@ def play_again(self): def restart_game(self): self.snake = Snake() self.score = 0 - self.place_food() \ No newline at end of file + self.place_food() diff --git a/projects/Snake Game/tests/test_game.py b/projects/Snake Game/tests/test_game.py index 6d9eed82..ed3fdbf7 100644 --- a/projects/Snake Game/tests/test_game.py +++ b/projects/Snake Game/tests/test_game.py @@ -1,7 +1,9 @@ import unittest from unittest.mock import patch, MagicMock +import pygame from game import Game from constants import GameSettings, Point +from snake import Snake class TestGame(unittest.TestCase): @@ -48,6 +50,35 @@ def test_place_food(self): self.game.place_food() self.assertNotIn(self.game.food, self.game.snake.blocks) + @patch('pygame.event.get') + def test_play_again_y(self, mock_event_get): + mock_event_get.return_value = [MagicMock(type=pygame.KEYDOWN, key=pygame.K_y)] + value = self.game.play_again() + self.assertTrue(value) + + @patch('pygame.event.get') + def test_play_again_return(self, mock_event_get): + mock_event_get.return_value = [MagicMock(type=pygame.KEYDOWN, key=pygame.K_RETURN)] + value = self.game.play_again() + self.assertTrue(value) + + @patch('pygame.event.get') + def test_play_again_n(self, mock_event_get): + mock_event_get.return_value = [MagicMock(type=pygame.KEYDOWN, key=pygame.K_n)] + + @patch('pygame.event.get') + def test_play_again_esc(self, mock_event_get): + mock_event_get.return_value = [MagicMock(type=pygame.KEYDOWN, key=pygame.K_ESCAPE)] + + def test_restart_game(self): + self.game.snake = Snake(init_length=10) + self.game.score = 10 + + self.game.restart_game() + self.assertEqual(len(self.game.snake.blocks), 3) + self.assertEqual(self.game.score, 0) + self.assertIsNotNone(self.game.food) + if __name__ == '__main__': unittest.main() From a5a6caf276db9f66028b01f86f819550cdb6ed4c Mon Sep 17 00:00:00 2001 From: Lisa Nguyen Date: Tue, 4 Jun 2024 21:11:51 +0800 Subject: [PATCH 13/18] implement high score system --- projects/Snake Game/src/display.py | 18 +++++++++++++++++- projects/Snake Game/src/game.py | 26 ++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/projects/Snake Game/src/display.py b/projects/Snake Game/src/display.py index 327ceac4..e35b7cb6 100644 --- a/projects/Snake Game/src/display.py +++ b/projects/Snake Game/src/display.py @@ -12,11 +12,12 @@ def __init__(self): pygame.display.set_caption("Snake") self.clock = pygame.time.Clock() - def update_ui(self, snake, food, score): + def update_ui(self, snake, food, score, high_score): self.window.fill(RgbColors.BLACK) self.draw_snake(snake) self.draw_food(food) self.draw_score(score) + self.render_high_score(high_score) pygame.display.flip() def draw_snake(self, snake): @@ -37,6 +38,7 @@ def draw_food(self, food): ) def draw_score(self, score): + self.font = pygame.font.Font(None, 25) score_display = self.font.render(f"Score: {score}", True, RgbColors.WHITE) self.window.blit(score_display, [0, 0]) # score in top left window corner @@ -57,3 +59,17 @@ def render_play_again(self): display_box = play_again_display.get_rect(center=(self.width // 2, self.height // 2)) self.window.blit(play_again_display, display_box) pygame.display.flip() + + def render_high_score(self, high_score): + high_score_display = self.font.render(f"High Score: {high_score}", True, RgbColors.WHITE) + self.window.blit(high_score_display, [self.width - high_score_display.get_width(), 0]) + + def render_new_high_score(self, new_high_score): + new_high_score_display = self.font.render(f"New High Score: {new_high_score}", True, RgbColors.WHITE) + text_width = new_high_score_display.get_width() + text_height = new_high_score_display.get_height() + text_x = (self.width - text_width) // 2 + text_y = (self.height // 3) - (text_height // 2) + self.window.blit(new_high_score_display, + [text_x, text_y]) + pygame.display.flip() diff --git a/projects/Snake Game/src/game.py b/projects/Snake Game/src/game.py index b3d13940..9f5ba225 100644 --- a/projects/Snake Game/src/game.py +++ b/projects/Snake Game/src/game.py @@ -1,5 +1,6 @@ import pygame import random +import json from snake import Snake, Direction, Point from display import Display from constants import GameSettings @@ -12,18 +13,23 @@ def __init__(self): self.score = 0 self.food = None self.place_food() + self.high_score = self.load_high_score() def game_loop(self): while True: self.play_step() game_over, score = self.play_step() + self.update_high_score(self.high_score) if game_over: self.display.render_game_over() + if score > self.high_score: + self.display.render_new_high_score(score) + self.update_high_score(score) + self.high_score = self.load_high_score() self.display.render_play_again() if not self.play_again(): break self.restart_game() - print("Final Score:", self.score) pygame.quit() def is_collision(self): @@ -67,7 +73,7 @@ def play_step(self): else: self.snake.blocks.pop() # Update UI and Clock - self.display.update_ui(self.snake, self.food, self.score) + self.display.update_ui(self.snake, self.food, self.score, self.high_score) self.display.clock.tick(GameSettings.SPEED) game_over = self.is_collision() return game_over, self.score @@ -96,3 +102,19 @@ def restart_game(self): self.snake = Snake() self.score = 0 self.place_food() + self.high_score = self.load_high_score() + + def load_high_score(self): + try: + with open('high_score.json', 'r') as file: + data = json.load(file) + return data.get('high_score') + except FileNotFoundError: + return 0 + + def update_high_score(self, new_score): + high_score = self.load_high_score() + if new_score > high_score: + data = {"high_score": new_score} + with open('high_score.json', 'w') as file: + json.dump(data, file) From d9cb648e3eb8420f688a9373552194d07d0ec5e0 Mon Sep 17 00:00:00 2001 From: Lisa Nguyen Date: Wed, 5 Jun 2024 22:04:52 +0800 Subject: [PATCH 14/18] add unit tests for game and display --- projects/Snake Game/tests/test_display.py | 107 +++++++++++++++++++++- projects/Snake Game/tests/test_game.py | 50 +++++++++- 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/projects/Snake Game/tests/test_display.py b/projects/Snake Game/tests/test_display.py index 335ac7ee..3a4208de 100644 --- a/projects/Snake Game/tests/test_display.py +++ b/projects/Snake Game/tests/test_display.py @@ -12,6 +12,8 @@ class TestDisplay(unittest.TestCase): def setUp(self): self.display = Display() self.snake = Snake() + self.display.width = 640 + self.display.height = 480 @patch('pygame.init') @patch('pygame.font.Font') @@ -67,12 +69,115 @@ def test_update_ui(self, mock_set_mode, mock_display_flip, mock_font, mock_draw_ self.score = 10 self.display.window = mock_window self.display.font = mock_font_instance - self.display.update_ui(self.snake, self.food, self.score) + self.high_score = 100 + self.display.update_ui(self.snake, self.food, self.score, self.high_score) mock_draw_rect.assert_called() mock_font_instance.render.assert_called() mock_display_flip.assert_called() + @patch('pygame.draw.rect') + def test_draw_snake(self, mock_draw_rect): + snake = MagicMock() + snake.blocks = [Point(0, 0), Point(GameSettings.BLOCK_SIZE, 0), Point(2 * GameSettings.BLOCK_SIZE, 0)] + + self.display.draw_snake(snake) + + # Check for correct snake block rendering + mock_draw_rect.assert_any_call( + self.display.window, RgbColors.BLUE1, pygame.Rect(0, 0, GameSettings.BLOCK_SIZE, GameSettings.BLOCK_SIZE) + ) + mock_draw_rect.assert_any_call( + self.display.window, RgbColors.BLUE2, pygame.Rect(4, 4, 12, 12) + ) + + @patch('pygame.draw.rect') + def test_draw_food(self, mock_draw_rect): + food = Point(0, 0) + self.display.draw_food(food) + # Check for correct food rendering + mock_draw_rect.assert_called_once_with( + self.display.window, RgbColors.RED, pygame.Rect(0, 0, GameSettings.BLOCK_SIZE, GameSettings.BLOCK_SIZE) + ) + + @patch('pygame.font.Font') + def test_draw_score(self, mock_font): + score = 10 + mock_font_instance = mock_font.return_value + mock_render = MagicMock() + mock_font_instance.render.return_value = mock_render + mock_render.get_width.return_value = 160 + mock_render.get_height.return_value = 120 + mock_window_surface = MagicMock() + self.display.window = mock_window_surface + self.display.font = mock_font_instance + self.display.draw_score(score) + + mock_font_instance.render.assert_called_once() + mock_window_surface.blit.assert_called_once() + + @patch('pygame.display.flip') + @patch('pygame.font.Font') + def test_render_game_over(self, mock_font, mock_flip): + mock_font_instance = mock_font.return_value + mock_render = MagicMock() + mock_font_instance.render.return_value = mock_render + mock_render.get_width.return_value = 160 + mock_render.get_height.return_value = 120 + mock_window_surface = MagicMock() + self.display.window = mock_window_surface + self.display.render_game_over() + + mock_font_instance.render.assert_called_once() + mock_window_surface.blit.assert_called_once() + mock_flip.assert_called_once() + + @patch('pygame.display.flip') + @patch('pygame.font.Font') + def test_render_play_again(self, mock_font, mock_flip): + mock_font_instance = mock_font.return_value + mock_render = MagicMock() + mock_font_instance.render.return_value = mock_render + mock_render.get_rect.return_value = pygame.Rect(0, 0, 100, 50) + mock_window_surface = MagicMock() + self.display.window = mock_window_surface + self.display.render_play_again() + + mock_font_instance.render.assert_called_once() + mock_window_surface.blit.assert_called_once() + mock_flip.assert_called_once() + + @patch('pygame.font.Font') + def test_render_high_score(self, mock_font): + high_score = 100 + mock_font_instance = mock_font.return_value + mock_render = MagicMock() + mock_font_instance.render.return_value = mock_render + mock_render.get_width.return_value = 160 + mock_render.get_height.return_value = 120 + mock_window_surface = MagicMock() + self.display.window = mock_window_surface + self.display.font = mock_font_instance + self.display.draw_score(high_score) + + mock_font_instance.render.assert_called_once() + mock_window_surface.blit.assert_called_once() + + @patch('pygame.display.flip') + @patch('pygame.font.Font') + def test_render_new_high_score(self, mock_font, mock_flip): + mock_font_instance = mock_font.return_value + mock_render = MagicMock() + mock_font_instance.render.return_value = mock_render + mock_render.get_rect.return_value = pygame.Rect(0, 0, 100, 50) + mock_window_surface = MagicMock() + self.display.window = mock_window_surface + self.display.render_play_again() + + mock_font_instance.render.assert_called_once() + mock_window_surface.blit.assert_called_once() + mock_flip.assert_called_once() + if __name__ == '__main__': unittest.main() diff --git a/projects/Snake Game/tests/test_game.py b/projects/Snake Game/tests/test_game.py index ed3fdbf7..192c5d22 100644 --- a/projects/Snake Game/tests/test_game.py +++ b/projects/Snake Game/tests/test_game.py @@ -1,5 +1,6 @@ +import io import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import patch, MagicMock, mock_open import pygame from game import Game from constants import GameSettings, Point @@ -28,10 +29,12 @@ def test_is_collision(self): @patch('pygame.draw.rect') @patch('pygame.display.flip') @patch('pygame.font.Font') - def test_play_step(self, mock_event_get, mock_draw_rect, mock_display_flip, mock_font): + def test_play_step(self, mock_font, mock_display_flip, mock_draw_rect, mock_event_get): mock_event_get.return_value = [] mock_font_instance = MagicMock() - mock_font_instance.render.return_value = MagicMock() + self.game.display.window = MagicMock(spec=pygame.Surface) + mock_surface = MagicMock(spec=pygame.Surface) + mock_font_instance.render.return_value = mock_surface mock_font.return_value = mock_font_instance init_snake_length = len(self.game.snake.blocks) @@ -43,6 +46,7 @@ def test_play_step(self, mock_event_get, mock_draw_rect, mock_display_flip, mock self.assertEqual(len(self.game.snake.blocks), init_snake_length + 1) self.assertEqual(self.game.score, init_score + 1) + # Check snake head is one block in front of the initial head position new_head_position = Point(init_head_position.x + GameSettings.BLOCK_SIZE, init_head_position.y) self.assertEqual(self.game.snake.head, new_head_position) @@ -73,11 +77,51 @@ def test_play_again_esc(self, mock_event_get): def test_restart_game(self): self.game.snake = Snake(init_length=10) self.game.score = 10 + init_high_score = self.game.high_score self.game.restart_game() self.assertEqual(len(self.game.snake.blocks), 3) self.assertEqual(self.game.score, 0) self.assertIsNotNone(self.game.food) + self.assertEqual(init_high_score, self.game.high_score) + + @patch('builtins.open', side_effect=FileNotFoundError) + def test_load_high_score_no_json_file(self, mock_open): + returned_high_score = self.game.load_high_score() + self.assertEqual(returned_high_score, 0) + + @patch('builtins.open', return_value=io.StringIO('{"high_score": 100}')) + def test_load_high_score_existing_file(self, mock_open): + returned_high_score = self.game.load_high_score() + self.assertEqual(returned_high_score, 100) + + @patch('builtins.open', new_callable=mock_open) + @patch('json.dump') + @patch('json.load', return_value={"high_score": 100}) + def test_update_high_score(self, mock_load, mock_dump, mock_open): + mock_file = mock_open.return_value + mock_file.__enter__.return_value = mock_file + + self.game.update_high_score(200) + + # Check file is opened in read-mode for loading high score, and opened in write-mode for updating high score + mock_open.assert_any_call('high_score.json', 'r') + mock_open.assert_any_call('high_score.json', 'w') + + mock_dump.assert_called_with({"high_score": 200}, mock_file) + + @patch('builtins.open', new_callable=mock_open) + @patch('json.dump') + @patch('json.load', return_value={"high_score": 100}) + def test_update_high_score_with_score_lower_than_high_score(self, mock_load, mock_dump, mock_open): + mock_file = mock_open.return_value + mock_file.__enter__.return_value = mock_file + + self.game.update_high_score(50) + + # Check high score is not changed + mock_open.assert_called_once_with('high_score.json', 'r') + mock_dump.assert_not_called() if __name__ == '__main__': From 69d655f6778f3f96ac8552ebe9c1e0838290b5ff Mon Sep 17 00:00:00 2001 From: Lisa Nguyen Date: Wed, 5 Jun 2024 22:50:14 +0800 Subject: [PATCH 15/18] update documentation --- projects/Snake Game/src/display.py | 11 ++++++++++- projects/Snake Game/src/game.py | 12 ++++++++++++ projects/Snake Game/src/snake.py | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/projects/Snake Game/src/display.py b/projects/Snake Game/src/display.py index e35b7cb6..898031a1 100644 --- a/projects/Snake Game/src/display.py +++ b/projects/Snake Game/src/display.py @@ -3,6 +3,7 @@ class Display: + """Manages the display of the game.""" def __init__(self): pygame.init() self.width = GameSettings.WIDTH @@ -13,6 +14,14 @@ def __init__(self): self.clock = pygame.time.Clock() def update_ui(self, snake, food, score, high_score): + """Updates the UI with the current game state. + + Args: + snake (Snake): The snake object that contains the snake body (Snake.blocks). + food (Point): The food object to be displayed. + score (int): The current game score. + high_score: The highest score achieved so far. + """ self.window.fill(RgbColors.BLACK) self.draw_snake(snake) self.draw_food(food) @@ -40,7 +49,7 @@ def draw_food(self, food): def draw_score(self, score): self.font = pygame.font.Font(None, 25) score_display = self.font.render(f"Score: {score}", True, RgbColors.WHITE) - self.window.blit(score_display, [0, 0]) # score in top left window corner + self.window.blit(score_display, [0, 0]) def render_game_over(self): self.font = pygame.font.Font(None, 48) diff --git a/projects/Snake Game/src/game.py b/projects/Snake Game/src/game.py index 9f5ba225..86796078 100644 --- a/projects/Snake Game/src/game.py +++ b/projects/Snake Game/src/game.py @@ -7,6 +7,7 @@ class Game: + """Manages the gameplay logic and its user interactions.""" def __init__(self): self.display = Display() self.snake = Snake() @@ -33,6 +34,11 @@ def game_loop(self): pygame.quit() def is_collision(self): + """Checks if the snake has collided with the boundary or with itself. + + Returns: + bool: True if a collision is detected, False otherwise. + """ # Snake hits boundary if ( self.snake.head.x > self.display.width - self.snake.block_size @@ -65,6 +71,7 @@ def get_user_input(self): self.snake.direction = Direction.DOWN def play_step(self): + """Executes one step through the game.""" self.get_user_input() self.snake.move(self.snake.direction) if self.snake.head == self.food: @@ -79,6 +86,7 @@ def play_step(self): return game_over, self.score def place_food(self): + """Randomly places the food on the screen.""" x = random.randint(0, ( self.display.width - GameSettings.BLOCK_SIZE) // GameSettings.BLOCK_SIZE) * GameSettings.BLOCK_SIZE y = random.randint(0, ( @@ -88,6 +96,7 @@ def place_food(self): self.place_food() def play_again(self): + """Asks the user to play again or quit the game.""" while True: for event in pygame.event.get(): if event.type == pygame.QUIT: @@ -99,12 +108,14 @@ def play_again(self): return True def restart_game(self): + """Resets the state of the game.""" self.snake = Snake() self.score = 0 self.place_food() self.high_score = self.load_high_score() def load_high_score(self): + """Loads the high score from a JSON file.""" try: with open('high_score.json', 'r') as file: data = json.load(file) @@ -113,6 +124,7 @@ def load_high_score(self): return 0 def update_high_score(self, new_score): + """Updates the high score in the JSON file if the new score is greater than the current high score.""" high_score = self.load_high_score() if new_score > high_score: data = {"high_score": new_score} diff --git a/projects/Snake Game/src/snake.py b/projects/Snake Game/src/snake.py index 09ca36f8..1fc3e89f 100644 --- a/projects/Snake Game/src/snake.py +++ b/projects/Snake Game/src/snake.py @@ -2,7 +2,13 @@ class Snake: + """Represents the snake in the game.""" def __init__(self, init_length=3): + """Initializes the snake. + + Args: + init_length (int): Length of the snake on initialization. + """ self.head = Point(GameSettings.WIDTH / 2, GameSettings.HEIGHT / 2) self.block_size = GameSettings.BLOCK_SIZE self.blocks = ([self.head] + @@ -10,6 +16,14 @@ def __init__(self, init_length=3): self.direction = Direction.RIGHT def move(self, direction): + """Moves the snake in the given direction. + + Args: + direction (Direction): The direction to move the snake. + + Returns: + Point: The new snake head position. + """ x, y = self.head if direction == Direction.RIGHT: x += self.block_size @@ -24,6 +38,11 @@ def move(self, direction): return self.head def self_collision(self): + """Checks if the snake collides with itself. + + Returns: + bool: True if the snake collides with its body, False otherwise. + """ if self.head in self.blocks[1:]: return True return False From 43233300384209155f60da9bde10be4565f973cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:54:18 +0000 Subject: [PATCH 16/18] contrib-readme-action has updated readme --- README.md | 305 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 156 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index 9837fccb..bae38189 100644 --- a/README.md +++ b/README.md @@ -875,25 +875,25 @@ - - shashaaankkkkk + + anish2105
- Shashank Shekhar + Anish Vantagodi
- - Gabriela20103967 + + JohnRTitor
- Gabriela20103967 + Masum Reza
- - justinjohnson-dev + + sudipg4112001
- Justin Johnson + Sudip Ghosh
@@ -904,32 +904,32 @@ - - sudipg4112001 + + justinjohnson-dev
- Sudip Ghosh + Justin Johnson
- - JohnRTitor + + shashaaankkkkk
- Masum Reza + Shashank Shekhar
- - anish2105 + + Gabriela20103967
- Anish Vantagodi + Gabriela20103967
- - bim22614 + + shriyansnaik
- Bim22614 + Shriyans Naik
@@ -940,24 +940,24 @@ - - shriyansnaik + + ng-lis
- Shriyans Naik + ng-lis
- - payallenka + + bim22614
- Payallenka + Bim22614
- - vagxrth + + payallenka
- Vagarth Pandey + Payallenka
@@ -969,31 +969,31 @@ - - herepete + + vagxrth
- Peter White + Vagarth Pandey
- - DevTomilola-OS + + nayan2306
- Oluwatomilola + Nayan Chandak
- - nayan2306 + + DevTomilola-OS
- Nayan Chandak + Oluwatomilola
- - snehafarkya + + herepete
- Sneha Farkya + Peter White
@@ -1003,14 +1003,21 @@ Syed Shagufta Noval + + + snehafarkya +
+ Sneha Farkya +
+ + ZackeryRSmith
Zackery .R. Smith
- - + SirRomey @@ -1045,15 +1052,15 @@
Suhas Kowligi
- + + odhyp
Odhy
- - + SubramanyaKS @@ -1088,15 +1095,15 @@
Yash Parwal
- + + Harry830
Harry830
- - + MrB141107 @@ -1131,15 +1138,15 @@
Vishal S Murali
- + + Albinary
Albert
- - + ayan-joshi @@ -1174,15 +1181,15 @@
Cryptoracing
- + + highb33kay
Ibukun Alesinloye
- - + mmakrin @@ -1217,15 +1224,15 @@
Srujan-landeri
- + + exeayush18
Ayush Singh
- - + ChefYeshpal @@ -1260,15 +1267,15 @@
Subha Sadhu
- + + u749929
U749929
- - + Adrija-G @@ -1303,15 +1310,15 @@
Anand Munjuluri
- + + oyeanmol
Anmol Shah
- - + IamSoo @@ -1346,15 +1353,15 @@
SHIVA NC
- + + rajdeepdas2000
Rajdeep Das
- - + Preeray @@ -1389,15 +1396,15 @@
Monish-Kumar-D
- + + RishavRaj20
Rishav Raj
- - + robertlent @@ -1432,15 +1439,15 @@
Rudy3
- + + iamshreeram
Shreeram
- - + siddharth9300 @@ -1475,15 +1482,15 @@
Utkarsh Raj
- + + KhwajaYousuf
KhwajaYousuf
- - + dapoadedire @@ -1518,15 +1525,15 @@
Ishan Joshi
- + + rakinplaban
Rakin Shahriar Plaban
- - + smw-1211 @@ -1561,15 +1568,15 @@
Anukiran Ghosh
- + + Akarsh3053
Akarsh Bajpai
- - + amoghakancharla @@ -1604,15 +1611,15 @@
CJ Praveen
- + + iamdestinychild
Destiny
- - + Devparihar5 @@ -1647,15 +1654,15 @@
Govind S Nair
- + + ducheharsh
Harsh Mahadev Duche
- - + HridayAg0102 @@ -1690,15 +1697,15 @@
MINHAJ
- + + manav0702
Manav Sanghvi
- - + yashd26 @@ -1733,15 +1740,15 @@
Mr._CG_04
- + + QuantumNovice
Haseeb / ν•˜μ‹œλΈŒ
- - + ambushneupane @@ -1776,15 +1783,15 @@
Ylavish64
- + + YashTariyal
Yash Tariyal
- - + wre9-tesh @@ -1819,15 +1826,15 @@
Harshit Sharma
- + + NishantPacharne
Nishant Pacharne
- - + Vivek-GuptaXCode @@ -1862,15 +1869,15 @@
Swapnadeep Mohapatra
- + + sumitbaroniya
Sumit Baroniya
- - + SulimanSagindykov @@ -1905,15 +1912,15 @@
Shivam Rai
- + + Sergey18273
Sergey18273
- - + sdthinlay @@ -1948,15 +1955,15 @@
Shayaanyar
- + + samualmartin
Samual Martin
- - + saipranavrguduru @@ -1991,15 +1998,15 @@
Abhishek Ghimire
- + + partheee
Parthiban Marimuthu
- - + ocryptocode @@ -2034,15 +2041,15 @@
Tharindu De Silva
- + + mojiibraheem
Zainab Ibraheem
- - + 0silverback0 @@ -2077,15 +2084,15 @@
Kish
- + + jonascarvalh
Jonas Carvalho
- - + jakbin @@ -2120,15 +2127,15 @@
Himanshu Singh Negi
- + + harshhes
HarsH
- - + GargiMittal @@ -2163,15 +2170,15 @@
Dhruvil-Lakhtaria
- + + Dhandeep10
Dhandeep
- - + David-hosting @@ -2206,15 +2213,15 @@
Ayushi Rastogi
- + + AtharvaDeshmukh0909
AtharvaDeshmukh0909
- - + Astrasv @@ -2249,15 +2256,15 @@
Anisha Nayaju
- + + Nini010
Aleena
- - + AdityaSahai123 @@ -2292,15 +2299,15 @@
ARYAN GULATI
- + + samayita1606
Samayita Kali
- - + Sai-Uttej-R @@ -2323,10 +2330,10 @@ - - RavenyBoi + + VedSadh
- Raveny + Ved Sadh
@@ -2335,15 +2342,15 @@
Ramii Ahmed - + + Raashika0201
Raashika0201
- - + farisfaikar @@ -2378,15 +2385,15 @@
Prajwol Shrestha
- + + KushalPareek
Kushal Pareek
- - + NooBIE-Nilay @@ -2421,15 +2428,15 @@
Mohamed Khaled Yousef
- + + Manishak798
Manisha Kundnani
- - + manishkumar00208 @@ -2464,15 +2471,15 @@
Jyothika Dileepkumar
- + + Josephtobi
Josephtobi
- - + Jishnu2608 From 642c40038cc404d31cc677369c492594491df3f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:55:15 +0000 Subject: [PATCH 17/18] contrib-readme-action has updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bae38189..99fc09a3 100644 --- a/README.md +++ b/README.md @@ -943,7 +943,7 @@ ng-lis
- ng-lis + Ng-lis
From 6100185213638f76acb08977982aa0f2442e9322 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:59:06 +0000 Subject: [PATCH 18/18] contrib-readme-action has updated readme --- README.md | 73 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 99fc09a3..76668ca8 100644 --- a/README.md +++ b/README.md @@ -825,25 +825,25 @@ - - kanchanrai7 + + ca20110820
- Kanchan Rai + Cedric Anover
- - TERNION-1121 + + kanchanrai7
- Vikrant Singh Bhadouriya + Kanchan Rai
- - ca20110820 + + TERNION-1121
- Cedric Anover + Vikrant Singh Bhadouriya
@@ -1578,17 +1578,17 @@ - - amoghakancharla + + blindaks
- Amogha Kancharla + Akrati Verma
- - blindaks + + amoghakancharla
- Akrati Verma + Amogha Kancharla
@@ -1714,17 +1714,17 @@ - - Morbius00 + + rust-master
- Raj Saha + Rust Master
- - rust-master + + Morbius00
- Rust Master + Raj Saha
@@ -2115,10 +2115,10 @@ - - JenilGajjar20 + + samayita1606
- Jenil Gajjar + Samayita Kali
@@ -2258,6 +2258,13 @@ + + + amersbahi +
+ Amer Sbahi +
+ Nini010 @@ -2292,21 +2299,14 @@
Abhay-Gupta008
- + + aryangulati
ARYAN GULATI
- - - - - samayita1606 -
- Samayita Kali -
@@ -2486,6 +2486,13 @@
Jishnudeep Borah
+ + + + JenilGajjar20 +
+ Jenil Gajjar +