diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index a8d9ab6..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM ghcr.io/league-infrastructure/jtlpython:20240719 -ENV VNC_RESOLUTION=600x600x16 - diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1c48802..75750cd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,32 +3,22 @@ { "name": "Python 3", - // This is the default - // "image": "mcr.microsoft.com/devcontainers/universal:2", - - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - //"image": "mcr.microsoft.com/devcontainers/python:1-${templateOption:imageVariant}", - //"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bookworm", - - "build": { - "dockerfile": "Dockerfile", - "context": ".." - }, + "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bookworm", "containerEnv": { + "VNC_RESOLUTION": "600x600x16", "SDL_VIDEO_WINDOW_POS": "0,0", // GUI Windows all the way in the upper left. "SDL_AUDIODRIVER": "dummy" // Disable audio }, // Features to add to the dev container. More info: https://containers.dev/features. "features": { - "ghcr.io/devcontainers/features/desktop-lite:1": { + "ghcr.io/devcontainers/features/desktop-lite:1.2.7": { "password" : "code4life" } }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [6080, 5901], @@ -48,8 +38,7 @@ "customizations": { "codespaces": { "openFiles": [ - //"lessons/00_Turtles/01_Get_Started.ipynb" - "README.md" + "lessons/00_Getting_Started/README.md" ] }, "vscode": { diff --git a/.devcontainer/package.json b/.devcontainer/package.json new file mode 100644 index 0000000..e69de29 diff --git a/.devcontainer/vscode-setup.ts b/.devcontainer/vscode-setup.ts new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 4f7b9e4..93f2c97 100644 --- a/README.md +++ b/README.md @@ -5,21 +5,25 @@ https://python-apprentice.jointheleague.org/) class, you are ready to continue your Python journey by learning to program games. To get started, either clone this repository to your local machine, or fork and -create a Codespace, then open the file [lessons/00_Getting_Started/README.md](lessons/00_Getting_Started/README.md). +create a Codespace, then open the file +[lessons/00_Getting_Started/README.md](lessons/00_Getting_Started/README.md). This will give you an overview of the module and how to get started. -The easiest way to get started is, from the [Python-Games Github Repo](https://github.com/league-curriculum/Python-Games), -click on the green ![code](https://images.jointheleague.org/github/code_button_sm.png) button, then select the 'Codespaces' tab, then ![ccom](https://images.jointheleague.org/github/create_codespace_sm.png). -This will create a codespace for you to work in, which has a web based IDE and all the necessary tools to get started. +The easiest way to get started is, from the [Python-Games Github Repo](https://github.com/league-curriculum/Python-Games), click on the green +![code](https://images.jointheleague.org/github/code_button_sm.png) button, then +select the 'Codespaces' tab, then +![ccom](https://images.jointheleague.org/github/create_codespace_sm.png). This +will create a codespace for you to work in, which has a web based IDE and all +the necessary tools to get started. -It's easiest to read the lesson documentation on Github; just click on the link in the `Next Steps` section below, but you -will also see all of the documentation in your coding editor. +It's easiest to read the lesson documentation on Github; just click on the link +in the `Next Steps` section below, but you will also see all of the +documentation in your coding editor. ## Next Steps -Open the file [lessons/00_Getting_Started/README.md](lessons/00_Getting_Started/README.md) to begin the lessons. +Open the file +[lessons/00_Getting_Started/README.md](lessons/00_Getting_Started/README.md) to +begin the lessons. -------------------- - -Development of The LEAGUE's curriculum is generously funded by the Itzkowitz Family Foundation. diff --git a/lessons/01_Motion_and_Physics/05_gravity_bounce.py b/lessons/01_Motion_and_Physics/05_gravity_bounce.py deleted file mode 100644 index d176005..0000000 --- a/lessons/01_Motion_and_Physics/05_gravity_bounce.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Gravity bounce with x motion - -If we add X velocity, from side to side, the player will bounce around the -screen. We will need to add a check to see if the player hits the left or right -side of the screen. - -""" -import pygame -from dataclasses import dataclass - -@dataclass -class Settings: - """Class for keeping track of game settings and constants.""" - screen_width: int = 500 - screen_height: int = 500 - white: tuple = (255, 255, 255) - black: tuple = (0, 0, 0) - red: tuple = (255, 0, 0) - player_size: int = 20 - gravity: int = 1 - jump_y_velocity: int = 30 - jump_x_velocity: int = 10 - -# Initialize Pygame -pygame.init() - -# Create an instance of Settings -settings = Settings() - -# Initialize screen -screen = pygame.display.set_mode((settings.screen_width, settings.screen_height)) - -# Define player -player = pygame.Rect(100, settings.screen_height - settings.player_size, settings.player_size, settings.player_size) - -player_y_velocity = 0 -player_x_velocity = 0 -x_direction = 1 # Elther 1 or negative 1, so we can keep track for direction after hitting the ground - -is_jumping = False - -# Main game loop -running = True -clock = pygame.time.Clock() - -while running: - - # Handle events, such as quitting the game - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - # Continuously jump. If the player is not jumping, make it jump - if is_jumping is False: - # Jumping means that the player is going up. The top of the - # screen is y=0, and the bottom is y=settings.screen_height. So, to go up, - # we need to have a negative y velocity - - player_y_velocity = -settings.jump_y_velocity - player_x_velocity = settings.jump_x_velocity * x_direction - - is_jumping = True - - else: # the player is jumping - # Update player position. Gravity is always pulling the player down, - # which is the positive y direction, so we add settings.gravity to the y velocity - # to make the player go up more slowly. Eventually, the player will have - # a positive y velocity, and gravity will pull the player down. - - player_y_velocity += settings.gravity - player.y += player_y_velocity - player.x += player_x_velocity - - # If the player hits one side of the screen or the other, bounce the player - if player.left <= 0 or player.right >= settings.screen_width: - player_x_velocity = -player_x_velocity - - # One way to change direction. - x_direction = -x_direction - # But this way is more reliable, since it will always be 1 or -1 and dir is tied to velocity - x_direction = player_x_velocity // abs(player_x_velocity) - - # If the player hits the top of the screen, bounce the player - if player.top <= 0: - player_y_velocity = -player_y_velocity - - # If the player hits the ground, stop the player from falling. - if player.bottom > settings.screen_height: - player.bottom = settings.screen_height - player_y_velocity = 0 - - player_x_velocity = 0 - - is_jumping = False - - - - # Draw everything - screen.fill(settings.white) - pygame.draw.rect(screen, settings.black, player) - - pygame.display.flip() - clock.tick(30) - -pygame.quit() diff --git a/lessons/01_Motion_and_Physics/00_Motion_and_Physics.ipynb b/lessons/01_Physics_for_Games/00_Physics_for_Games.ipynb similarity index 85% rename from lessons/01_Motion_and_Physics/00_Motion_and_Physics.ipynb rename to lessons/01_Physics_for_Games/00_Physics_for_Games.ipynb index 0d7c16e..e4b715e 100644 --- a/lessons/01_Motion_and_Physics/00_Motion_and_Physics.ipynb +++ b/lessons/01_Physics_for_Games/00_Physics_for_Games.ipynb @@ -4,8 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Motion and Physics\n", - "\n", + "# Physics for Games\n", "\n", "Most of the games we will be writing have some physics, which is the motion of\n", "objects in the game that should look realistic, like jumping or falling. Pygame\n", @@ -20,7 +19,7 @@ "\n", "## Assignment 1\n", "\n", - "1. Open the program `01-move.py` \n", + "1. Open the program `01_move.py` \n", "2. Run the program and see what it does. Use the arrow keys to move the square\n", " around the screen.\n", "3. Read the code and try to understand how it works.\n", @@ -83,7 +82,7 @@ "\n", "## Assignment 3\n", "\n", - "The most common game mechanic with acelleration is jumping. Let's look at\n", + "The most common game mechanic with acceleration is jumping. Let's look at\n", "what it takes to make a player jump.\n", "\n", "1. Open `04_gravity.py` \n", @@ -112,9 +111,10 @@ "collision is to just not let the player move into the wall, but a more realistic\n", "way is to bounce the player off the wall.\n", "\n", - "Collisions with the edges of the screen are simple to detect: the player's position in x or y either\n", - "exceeds the screen size or goes below zero. Making the player bounce is also easy: just reverse the\n", - "velocity in the direction of the collision. \n", + "Collisions with the edges of the screen are simple to detect: the player's\n", + "position in x or y either exceeds the screen size or goes below zero. Making the\n", + "player bounce is also easy: just reverse the velocity in the direction of the\n", + "collision. \n", "\n", "```python\n", "if player.x < 0 or player.x > screen_width:\n", @@ -126,7 +126,7 @@ "\n", "## Assignment 2\n", "\n", - "1. Copy `05_gravity_bounce.py` into this directory.\n", + "1. Open and read through `05_gravity_bounce.py`.\n", "2. Change the program so that the player doesn't always jump. Instead, the player\n", " jumps only when the `space` key is pressed.\n", "3. Change the program so that the player can jump to the left or right by pressing\n", @@ -136,14 +136,18 @@ " less than 0.1, set it to zero. This will make the player slow down and stop.\n", " If the Players velocity is zero, the player can jump again. The player\n", " shoud bounce off the ground.\n", + "5. Change the program to account for elasticity. The ball should lose a bit of\n", + " energy each time it bounces. \n", "\n", "Hints: \n", "\n", - "* Jumping up will involve setting the y velocity, and jumping left or right\n", - "will also involve setting the x velocity.\n", + "* Jumping up will involve setting the y velocity, and jumping left or right will\n", + " also involve setting the x velocity.\n", "* You can implement drag either by subtracting off a little bit of the velocity,\n", " but you will need to make sure the velocity doesn't go negative. Another way\n", - " to do this is to multiply the velocity by a number less than 1, like 0.99.\n", + " to do this is to multiply the velocity by a number less than 1, like 0.99.\n", + "* Elasticity works a lot like drag, except it only causes a reduction in\n", + " velocity when the ball bounces, not every time step. \n", "\n", "\n", "## Next Steps\n", @@ -151,10 +155,12 @@ "First, check in your code! You should check in ( commit ) and push your code\n", "regularly. \n", "\n", - "Second, don't forget about the [Pygame Documentation](https://www.pygame.org/docs/index.html). Your programs will\n", - "get better the more you know about Pygame.\n", + "Second, don't forget about the \n", + "[Pygame Documentation](https://www.pygame.org/docs/index.html). \n", + "Your programs will get better the more you know about Pygame.\n", "\n", - "Open [lessons/02_Classes_and_Objects/README.md](../02_Classes_and_Objects/README.md) to start the next lesson. " + "Open [lessons/02_Classes_and_Objects/README.md](../02_Classes_and_Objects/README.md) \n", + "to start the next lesson. " ] } ], diff --git a/lessons/01_Motion_and_Physics/01_move.py b/lessons/01_Physics_for_Games/01_move.py similarity index 76% rename from lessons/01_Motion_and_Physics/01_move.py rename to lessons/01_Physics_for_Games/01_move.py index ab944bf..9ebc459 100644 --- a/lessons/01_Motion_and_Physics/01_move.py +++ b/lessons/01_Physics_for_Games/01_move.py @@ -8,6 +8,7 @@ """ import pygame + # Initialize Pygame pygame.init() @@ -16,9 +17,12 @@ SQUARE_SIZE = 50 SQUARE_COLOR = (0, 128, 255) # Red-Green-Blue color in the range 0-255 BACKGROUND_COLOR = (255, 255, 255) # White -SQUARE_SPEED = 5 +SQUARE_SPEED = 300 FPS = 60 +v = SQUARE_SPEED # Speed of the square in pixels per second +d_t = 1 / FPS # Time step for physics calculations + # Initialize the screen screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Move the Square") @@ -29,14 +33,13 @@ # Main function def main(): # Initial position of the square - square_x = SCREEN_WIDTH // 2 - SQUARE_SIZE // 2 - square_y = SCREEN_HEIGHT // 2 - SQUARE_SIZE // 2 + x = SCREEN_WIDTH // 2 - SQUARE_SIZE // 2 + y = SCREEN_HEIGHT // 2 - SQUARE_SIZE // 2 running = True while running: - # Event handling for event in pygame.event.get(): @@ -48,19 +51,31 @@ def main(): # with a boolean value of whether they are pressed or not keys = pygame.key.get_pressed() + # Calculate the change tin the position + d_x = 0 + d_y = 0 + # Move the square based on arrow keys if keys[pygame.K_LEFT]: - square_x -= SQUARE_SPEED + d_x = -v * d_t + if keys[pygame.K_RIGHT]: - square_x += SQUARE_SPEED + d_x = v * d_t + if keys[pygame.K_UP]: - square_y -= SQUARE_SPEED + d_y = -v * d_t + if keys[pygame.K_DOWN]: - square_y += SQUARE_SPEED + d_y = v * d_t + + # Update the position of the square + x = x + d_x + y = y + d_y + # Prevent the square from going off the screen - square_x = max(0, min(SCREEN_WIDTH - SQUARE_SIZE, square_x)) - square_y = max(0, min(SCREEN_HEIGHT - SQUARE_SIZE, square_y)) + x = max(0, min(SCREEN_WIDTH - SQUARE_SIZE, x)) + y = max(0, min(SCREEN_HEIGHT - SQUARE_SIZE, y)) # This will clear the screen by filling it # with the background color. If we didn't do this, @@ -68,7 +83,7 @@ def main(): screen.fill(BACKGROUND_COLOR) # Draw the square - pygame.draw.rect(screen, SQUARE_COLOR, (square_x, square_y, SQUARE_SIZE, SQUARE_SIZE)) + pygame.draw.rect(screen, SQUARE_COLOR, (x, y, SQUARE_SIZE, SQUARE_SIZE)) # Update the display. Imagine that the screen is two different whiteboards. One # whiteboard is currently visible to the player, and the other whiteboard is being diff --git a/lessons/01_Motion_and_Physics/02_no_acceleration.py b/lessons/01_Physics_for_Games/02_no_acceleration.py similarity index 72% rename from lessons/01_Motion_and_Physics/02_no_acceleration.py rename to lessons/01_Physics_for_Games/02_no_acceleration.py index e3edfb3..010908f 100644 --- a/lessons/01_Motion_and_Physics/02_no_acceleration.py +++ b/lessons/01_Physics_for_Games/02_no_acceleration.py @@ -10,15 +10,19 @@ SCREEN_WIDTH, SCREEN_HEIGHT = 600, 600 SQUARE_SIZE = 50 SQUARE_COLOR = (255, 0, 0) # Red -SQUARE_SPEED = 5 +SQUARE_SPEED = 300 + +FPS = 60 # Frames per second + +d_t = 1 / FPS # Time step for physics calculations # Set up the display screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Moving Red Square") # Square starting position -x_pos = 0 -y_pos = (SCREEN_HEIGHT - SQUARE_SIZE) // 2 +x = 0 +y = (SCREEN_HEIGHT - SQUARE_SIZE) // 2 # Movement direction: 1 for right, -1 for left direction = 1 @@ -31,25 +35,28 @@ running = False # Move the square, a bit each frame - x_pos += SQUARE_SPEED * direction + + d_x = SQUARE_SPEED * direction * d_t + + x += d_x # Check for screen bounds and reverse direction if necessary - if x_pos + SQUARE_SIZE > SCREEN_WIDTH: + if x + SQUARE_SIZE > SCREEN_WIDTH: direction = -1 # Move left - elif x_pos < 0: + elif x < 0: direction = 1 # Move right # Fill the screen with black (clears previous frame) screen.fill((0, 0, 0)) # Draw the red square - pygame.draw.rect(screen, SQUARE_COLOR, (x_pos, y_pos, SQUARE_SIZE, SQUARE_SIZE)) + pygame.draw.rect(screen, SQUARE_COLOR, (x, y, SQUARE_SIZE, SQUARE_SIZE)) # Update the display pygame.display.flip() # Frame rate control - pygame.time.Clock().tick(60) + pygame.time.Clock().tick(FPS) # Quit Pygame pygame.quit() diff --git a/lessons/01_Motion_and_Physics/03_acceleration.py b/lessons/01_Physics_for_Games/03_acceleration.py similarity index 75% rename from lessons/01_Motion_and_Physics/03_acceleration.py rename to lessons/01_Physics_for_Games/03_acceleration.py index 73f1591..95b36b5 100644 --- a/lessons/01_Motion_and_Physics/03_acceleration.py +++ b/lessons/01_Physics_for_Games/03_acceleration.py @@ -7,7 +7,8 @@ SCREEN_WIDTH, SCREEN_HEIGHT = 600, 600 SQUARE_SIZE = 50 SQUARE_COLOR = (255, 0, 0) # Red -K = .0004 +FPS = 60 +K = 3 # Spring constant, controls how strong the spring force is # Set up the display screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) @@ -16,6 +17,10 @@ # Square starting position x_pos = 20 y_pos = (SCREEN_HEIGHT - SQUARE_SIZE) // 2 + +d_t = 1 / FPS # Time step for physics calculations + +mass = 2.0 # Mass of the square, used to calculate acceleration velocity = 0 # Movement direction: 1 for right, -1 for left @@ -27,22 +32,25 @@ while running: for event in pygame.event.get(): if event.type == pygame.QUIT: - running = False # Calculate the spring force, which is the force that pulls the square back # to the center, as if it was attached to a spring - a = -K * (x_pos - (SCREEN_WIDTH-SQUARE_SIZE) // 2) + + + # Calculate the spring force, accounting for mass (F = -k*x) + # Force divided by mass gives acceleration (F = ma → a = F/m) + a = (-K * (x_pos - (SCREEN_WIDTH-SQUARE_SIZE) // 2)) / mass # Update the velocity with the acceleration. Notice that we change # the velocity by adding the acceleration, not setting it to the acceleration, # and we change it a bit each frame. - velocity += a + velocity += a * d_t # Update the position with the velocity. Like with the velocity, we change # the position by adding the velocity, not setting it to the velocity, and # we change it a bit each frame. - x_pos += velocity + x_pos += velocity * d_t # Fill the screen with black (clears previous frame) screen.fill((0, 0, 0)) @@ -54,7 +62,7 @@ pygame.display.flip() # Frame rate control - pygame.time.Clock().tick(60) + pygame.time.Clock().tick(FPS) # Quit Pygame pygame.quit() diff --git a/lessons/01_Motion_and_Physics/04_gravity.py b/lessons/01_Physics_for_Games/04_gravity.py similarity index 80% rename from lessons/01_Motion_and_Physics/04_gravity.py rename to lessons/01_Physics_for_Games/04_gravity.py index 26ff474..0a96361 100644 --- a/lessons/01_Motion_and_Physics/04_gravity.py +++ b/lessons/01_Physics_for_Games/04_gravity.py @@ -26,17 +26,19 @@ class GameSettings: screen_height: int = 500 player_size: int = 10 player_x: int = 100 # Initial x position of the player - gravity: float = 0.3 # acelleration, the change in velocity per frame - jump_velocity: int = 15 + + jump_velocity: int = 200 white: tuple = (255, 255, 255) black: tuple = (0, 0, 0) - tick_rate: int = 30 # Frames per second + + gravity: float = 60.0 # acceleration, the change in velocity per frame + d_t: float = 1.0/30 + m: float = 2.0 # mass of the player, used to calculate acceleration # Initialize game settings settings = GameSettings() - # Initialize screen screen = pygame.display.set_mode((settings.screen_width, settings.screen_height)) @@ -45,7 +47,6 @@ class GameSettings: settings.screen_height - settings.player_size, settings.player_size, settings.player_size) -player_y_velocity = 0 is_jumping = False # Main game loop @@ -64,15 +65,19 @@ class GameSettings: # Jumping means that the player is going up. The top of the # screen is y=0, and the bottom is y=SCREEN_HEIGHT. So, to go up, # we need to have a negative y velocity - player_y_velocity = -settings.jump_velocity + d_v_y = -settings.jump_velocity is_jumping = True - # Update player position. Gravity is always pulling the player down, - # which is the positive y direction, so we add GRAVITY to the y velocity - # to make the player go up more slowly. Eventually, the player will have - # a positive y velocity, and gravity will pull the player down. - player_y_velocity += settings.gravity - player.y += player_y_velocity + # acelleration in sht y direction + a_y = settings.gravity + + # Change in the velocity due to accelleration + d_v_y += a_y * settings.d_t + + # Change in the position due to the velocity + d_y = d_v_y * settings.d_t + + player.y += d_y # If the player hits the ground, stop the player from falling. # The player's position is measured from the top left corner, so the @@ -82,7 +87,7 @@ class GameSettings: # and stop the player from falling if player.bottom >= settings.screen_height: player.bottom = settings.screen_height - player_y_velocity = 0 + d_v_y = 0 is_jumping = False # Draw everything @@ -90,6 +95,6 @@ class GameSettings: pygame.draw.rect(screen, settings.black, player) pygame.display.flip() - clock.tick(settings.tick_rate) + clock.tick( int(1/settings.d_t)) pygame.quit() diff --git a/lessons/01_Physics_for_Games/05_gravity_bounce.py b/lessons/01_Physics_for_Games/05_gravity_bounce.py new file mode 100644 index 0000000..15d2166 --- /dev/null +++ b/lessons/01_Physics_for_Games/05_gravity_bounce.py @@ -0,0 +1,116 @@ +""" +Gravity bounce with x motion + +If we add X velocity, from side to side, the player will bounce around the +screen. We will need to add a check to see if the player hits the left or right +side of the screen. + +""" +import pygame +from dataclasses import dataclass + +@dataclass +class GameSettings: + """Class for keeping track of game settings and constants.""" + screen_width: int = 500 + screen_height: int = 500 + square_size: int = 20 + square_color: tuple = (0, 0, 0) # Black + background_color: tuple = (255, 255, 255) # White + fps: int = 30 + gravity: float = 60.0 # Acceleration due to gravity + jump_velocity_y: float = 200.0 # Initial jump velocity in y direction + jump_velocity_x: float = 100.0 # Initial jump velocity in x direction + d_t: float = 1.0/30 # Time step for physics calculations + +# Initialize Pygame +pygame.init() + +# Initialize game settings +settings = GameSettings() + +# Initialize screen +screen = pygame.display.set_mode((settings.screen_width, settings.screen_height)) +pygame.display.set_caption("Gravity Bounce") + +# Square starting position +x_pos = 100 +y_pos = settings.screen_height - settings.square_size + +# Initial velocities +velocity_x = 0 +velocity_y = 0 +x_direction = 1 # Either 1 or -1, to keep track of direction after hitting the ground + +is_jumping = False + +# Main loop +running = True +clock = pygame.time.Clock() + +while running: + + # Handle events, such as quitting the game + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # Continuously jump. If the square is not jumping, make it jump + if is_jumping is False: + # Jumping means that the square is going up. The top of the + # screen is y=0, and the bottom is y=screen_height. So, to go up, + # we need to have a negative y velocity + + velocity_y = -settings.jump_velocity_y + velocity_x = settings.jump_velocity_x * x_direction + + is_jumping = True + + else: # the square is jumping + # Update square position. Gravity is always pulling the square down, + # which is the positive y direction, so we add settings.gravity to the y velocity + # to make the square go up more slowly. Eventually, the square will have + # a positive y velocity, and gravity will pull the square down. + + velocity_y += settings.gravity * settings.d_t + + # Update the position with the velocity. Like with the velocity, we change + # the position by adding the velocity, not setting it to the velocity, and + # we change it a bit each frame. + y_pos += velocity_y * settings.d_t + x_pos += velocity_x * settings.d_t + + # If the square hits one side of the screen or the other, bounce the square + if x_pos <= 0 or x_pos + settings.square_size >= settings.screen_width: + velocity_x = -velocity_x + + # Update direction tracking + x_direction = -x_direction + # This way is more reliable, since it will always be 1 or -1 and direction is tied to velocity + if velocity_x != 0: + x_direction = int(velocity_x / abs(velocity_x)) + + # If the square hits the top of the screen, bounce the square + if y_pos <= 0: + velocity_y = -velocity_y + + # If the square hits the ground, stop the square from falling. + if y_pos + settings.square_size > settings.screen_height: + y_pos = settings.screen_height - settings.square_size + velocity_y = 0 + velocity_x = 0 + is_jumping = False + + # Fill the screen with background color (clears previous frame) + screen.fill(settings.background_color) + + # Draw the square + pygame.draw.rect(screen, settings.square_color, (x_pos, y_pos, settings.square_size, settings.square_size)) + + # Update the display + pygame.display.flip() + + # Frame rate control + clock.tick(settings.fps) + +pygame.quit() diff --git a/lessons/01_Motion_and_Physics/README.md b/lessons/01_Physics_for_Games/README.md similarity index 100% rename from lessons/01_Motion_and_Physics/README.md rename to lessons/01_Physics_for_Games/README.md diff --git a/lessons/02_Classes_and_Objects/00_Classes_and_Objects.ipynb b/lessons/02_Classes_and_Objects/00_Classes_and_Objects.ipynb index 4845473..9a9a69b 100644 --- a/lessons/02_Classes_and_Objects/00_Classes_and_Objects.ipynb +++ b/lessons/02_Classes_and_Objects/00_Classes_and_Objects.ipynb @@ -82,7 +82,7 @@ "Notice that we use the dot \".\" to reference things that are part of the object,\n", "so `alice.name` gets the `name` variable in the `alice` object \n", "\n", - "Now, we have both a class, `Person` and an obect of that class, `alice`.\n", + "Now, we have both a class, `Person` and an object of that class, `alice`.\n", "Remember our definition of an object from the start: it is a structure that has\n", "identity, state and behavior. \n", "\n", @@ -96,7 +96,8 @@ "metadata": {}, "source": [ "Now, of course, you actually already know a lot about objects. Here is part of\n", - "the first program in our first Python lesson :\n", + "the first program in our first Python lesson, back when you were writting turtle\n", + "programs:\n", "\n", "```python \n", "\n", @@ -151,7 +152,7 @@ "source": [ "## Assignment 1\n", "\n", - "Write a class for that describes an alien. The Alien will have these important variables:\n", + "Write a class that describes an alien. The Alien will have these important variables:\n", "\n", "* number of eyes\n", "* number of legs\n", @@ -169,11 +170,11 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "# Test Yourself\n", + "# Test Yourself by writing a class for an alien\n", "\n" ] }, @@ -184,7 +185,10 @@ "## Inheritance, Polymorphism and Overloading\n", "\n", "Inheritance is a way to create a new class that is based on an existing class. Inheritance allows you \n", - "to mosltly copy the existing class, but then add new variables and methods. For instance, here is our People class:\n", + "to mosltly copy the existing class, but then add new variables and methods. Just like a child might inherit a parents\n", + "eye color, classes can inhert properties from its parent. \n", + "\n", + "For instance, here is our People class:\n", "\n", "```python\n", "\n", @@ -247,7 +251,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -378,7 +382,7 @@ "## Assignment 3\n", "\n", "1. Read and run the program `01_Tom_the_Turtle.py` in this lession directory.\n", - "2. Create a derived class from the `Turtle` class that add some new behavior.\n", + "2. Create a derived class from the `Turtle` class that adds some new behavior.\n", " Add a function `right` that turns the turtle to the right. ( Bonus, use the\n", " `left` function to implement the `right` function )\n", "3. In your derived class, add a new variable `color` and a way to set it. Use\n", @@ -471,7 +475,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python-Games", "language": "python", "name": "python3" }, @@ -485,7 +489,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.0" + "version": "3.13.3" } }, "nbformat": 4, diff --git a/lessons/02_Classes_and_Objects/01_Tom_the_Turtle.py b/lessons/02_Classes_and_Objects/01_Tom_the_Turtle.py index 91be936..ec38e80 100644 --- a/lessons/02_Classes_and_Objects/01_Tom_the_Turtle.py +++ b/lessons/02_Classes_and_Objects/01_Tom_the_Turtle.py @@ -4,6 +4,28 @@ way to introduce programming, so let's make something similar in PyGame, using objects. +This program is for assignment 3 and 4 + +Assignment 3: + +1. Read and run the program `01_Tom_the_Turtle.py` in this lession directory. +2. Create a derived class from the `Turtle` class that adds some new behavior. + Add a function `right` that turns the turtle to the right. ( Bonus, use the + `left` function to implement the `right` function ) +3. In your derived class, add a new variable `color` and a way to set it. Use + that color to set the color of the turtle's line +4. Add a `pen_up` and `pen_down` function that will raise and lower the pen. + +Assignment 4: + +1. Create a function ( not a method, the function should not be part of a class) + in your `01_Tom_the_Turtle` program that takes a Turtle object and prints + out the x and y position of the turtle. +2. Show that your function works by creating a turtle, moving it around, and + then calling your function, for both the base `Turtle` class and your derived + class. + + """ import math diff --git a/lessons/02_Classes_and_Objects/02_Gravity_Bounce.ipynb b/lessons/02_Classes_and_Objects/02_Gravity_Bounce.ipynb index fba0f7d..7d44054 100644 --- a/lessons/02_Classes_and_Objects/02_Gravity_Bounce.ipynb +++ b/lessons/02_Classes_and_Objects/02_Gravity_Bounce.ipynb @@ -9,7 +9,7 @@ "Now that we have a basic understanding of how objects work, we can work on an\n", "updated version of the Gravity Bounce program. \n", "\n", - "## Assignment\n", + "## Assignment 5\n", "\n", "1. Open `03_gravity_bounce_obj.py` \n", "2. Review the program and try to understand how it works.\n", diff --git a/lessons/02_Classes_and_Objects/03_gravity_bounce_obj.py b/lessons/02_Classes_and_Objects/03_gravity_bounce_obj.py index d6e6375..1b7e2f7 100644 --- a/lessons/02_Classes_and_Objects/03_gravity_bounce_obj.py +++ b/lessons/02_Classes_and_Objects/03_gravity_bounce_obj.py @@ -6,6 +6,19 @@ a separate class. This makes the code easier to read and understand, and allows for more complex games with multiple objects. +## Assignment 5 + +1. Open `03_gravity_bounce_obj.py` +2. Review the program and try to understand how it works. +3. Change the program so that the player's initial velocity and position are set + in the initializer to the `Player` class. +4. Add a color for the player, configurable in the initializer. +5. Add a second player to the game. The second player should be a different + color and have different initial position and velocity. + +When you are done, your program should have two player objects ( but only one +Player class!), of different colors, bouncing around in different trajectories. + """ import pygame @@ -21,14 +34,20 @@ class GameSettings: """Settings for the game""" width: int = 500 height: int = 500 - gravity: float = 0.3 - player_start_x: int = 100 - player_start_y: int = None - player_v_y: float = 0 # Initial y velocity - player_v_x: float = 7 # Initial x velocity player_width: int = 20 player_height: int = 20 - player_jump_velocity: float = 15 + + player_start_x: int = 100 + player_start_y: int = None + + gravity: float = 200 + v_0_y: float = 0 # Initial y velocity + v_0_x: float = 75 # Initial x velocity + + jump_v_y: float = 400 + + FPS = 30 + d_t = 1 / FPS # Time step class Game: @@ -66,7 +85,7 @@ def run(self): player.draw(self.screen) pygame.display.flip() - self.clock.tick(60) + self.clock.tick(self.settings.FPS) pygame.quit() @@ -82,13 +101,13 @@ def __init__(self, game: Game): self.height = settings.player_height self.is_jumping = False - self.v_jump = settings.player_jump_velocity + self.v_jump = settings.jump_v_y self.y = settings.player_start_y if settings.player_start_y is not None else settings.height - self.height self.x = settings.player_start_x - self.v_x = settings.player_v_x # X Velocity - self.v_y = settings.player_v_y # Y Velocity + self.v_x = settings.v_0_x # X Velocity + self.v_y = settings.v_0_y # Y Velocity def update(self): """Update player position, continuously jumping""" @@ -98,8 +117,8 @@ def update(self): def update_y(self): """Update the player's y position based on gravity and velocity""" - self.v_y += self.game.settings.gravity # Add gravity to the y velocity - self.y += self.v_y # Update the player's y position, based on the current velocity + self.v_y += self.game.settings.gravity * self.game.settings.d_t # Add gravity to the y velocity + self.y += self.v_y * self.game.settings.d_t # Update the player's y position, based on the current velocity if self.y >= self.game.settings.height - self.height: self.y = self.game.settings.height - self.height @@ -108,7 +127,7 @@ def update_y(self): def update_x(self): """Update the player's x position based on horizontal velocity and bounce on edges""" - self.x += self.v_x # Update the player's x position based on the current velocity + self.x += self.v_x * self.game.settings.d_t # Update the player's x position based on the current velocity if self.x <= 0: self.x = 0 diff --git a/lessons/03_Vectors/00_Vectors.ipynb b/lessons/03_Vectors/00_Vectors.ipynb index b804133..8f2ec2c 100644 --- a/lessons/03_Vectors/00_Vectors.ipynb +++ b/lessons/03_Vectors/00_Vectors.ipynb @@ -16,7 +16,17 @@ "* The other is as a direction and a magnitude, which is a line with a length and\n", "a direction.\n", "\n", - "Here is what the vector (x=8, y=8) looks like:\n", + "These two was are actually the same. Any point in space, (x,y) is relative to\n", + "the origin, which is the point at (0,0), so for the first way of thinking about\n", + "a vector, a point in space, (x, y) we also imiagine that there is a line from\n", + "the origin (0,0) to the point (x,y), and that line will have a length and a\n", + "direction, which is our second way of thinking about vectors. \n", + "\n", + "There are a lot of videos and web pages to help you understand vectors in more\n", + "depth, [here is a good one](https://youtu.be/VqrYlDcZQ54?feature=shared).\n", + "\n", + "Here is what the vector (x=8, y=8) looks like when we show it on the X/Y coordinate plane, \n", + "starting from the origin. \n", "\n", "![Vector](images/v88.png)\n", "\n", @@ -25,13 +35,23 @@ "angle of 45 degrees. So, it can both be a point and a line, as long as we can\n", "assume a starting point for the line. \n", "\n", + "( But wait ... why are there only two numbers in a vector? Can there be more?\n", + "Yes! You can have any number of numbers in a vector. We are using 2 numbers\n", + "because our games are 2 dimensional. But if we were creating 3D games, our\n", + "vectors would have 3 numbers, and mathematicians and physicists often use\n", + "vectors that have more numbers. )\n", + "\n", + "Since we have a new kind of mathematical object, vectors, that have two numbers\n", + "in them, like (8,8), we are going to need a new work to refer to regular\n", + "numbers, like just plain old '8': these regular numbers are called \"scalars\".\n", + "\n", "Here is a really simple Vector class that we can use to represent vectors:\n", "\n" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -80,22 +100,140 @@ "\n", "\n", "As you can see, your create the vector by passing in the x and y values, and\n", - "then you can calculate the length and angle using trigonometry.\n", + "then you can calculate the length and angle using trigonometry.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are two very important things that you can do with a vector: we can\n", + "multiply a vector by a scalar, and we can add two vectors together. \n", + "\n", + "* vector * scalar: Multiply the elements of the vector by the scalar\n", + "* vector + vector: Add the elements in each position to create a new vector\n", + "\n", + "Let's suppose that we have the vectors (2,3) and (4,5) and the scalar 5.\n", + "\n", + "* (2,3) * 5 = ( 2*5, 3*5) = (10,15)\n", + "* (2,3) + (4,5) = ( 2+4, 3+5 ) \n", + "\n", + "Multiplying a vector by a scalar is called \"scaling\" the vector; it makes the\n", + "vector longer or shorter, but does not change its direction. Adding two vectors\n", + "combines the vectors and produces a new vectors that can have a different\n", + "magnitude and direction. \n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Assignment\n", + "\n", + "In the code cell below, update our Vector class with implmentations of the special methods for multiplication ( `__mul__`) and \n", + "addition ( `__add__`)\n", + "\n", + "You can check the type of an argument with code like this: \n", + "\n", + "```python\n", + "if isinstance(scalar, (int, float)):\n", + " print('Yes, it is a scalar')\n", + "else:\n", + " print('No not a scalar')\n", + "```\n", + "\n", + "\n", + "If the user has provided the wrong types for the arguments, you can signal an error with a line of code like this: \n", + "\n", + "```python\n", + " raise TypeError(\"Can only multiply Vector by a scalar (int or float)\")\n", + " ```\n", + "\n", + "Both of your implementations must return a new Vector, so they should have code like this:\n", + "\n", + "```python \n", + "return Vector( x_value, y_value)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Vector: Vector(4, 4)\n", + "Magnitude: 5.656854249492381\n", + "Direction: 45.0°\n" + ] + } + ], + "source": [ + "import math\n", + "\n", + "class Vector:\n", + " def __init__(self, x, y):\n", + " self.x = x\n", + " self.y = y\n", + "\n", + " @property\n", + " def magnitude(self): # magnitude is the length of the vector\n", + " # Magnitude of the vector: sqrt(x^2 + y^2)\n", + " return math.sqrt(self.x**2 + self.y**2)\n", + "\n", + " @property\n", + " def direction(self):\n", + " # Direction (angle) in radians: atan2(y, x)\n", + " return math.atan2(self.y, self.x) * 180 / math.pi\n", + "\n", + " def __repr__(self):\n", + " return f\"Vector({self.x}, {self.y})\"\n", + " \n", + "\n", + " def __mul__(self, scalar):\n", + " \"\"\"Multiply the vector by a scalar value.\"\"\"\n", + "\n", + " pass\n", + "\n", + " def __add__(self, other):\n", + " pass\n", "\n", - "If we have a vector that is a position, we can add two vectors together to get a\n", - "new vector that is the sum of the two. This is called vector addition. If we\n", - "have a vector that is a direction and magnitude, we can multiply the vector by a\n", - "scalar ( a single number ) to get a new vector that is the original vector\n", - "multiplied by the scalar. This is called scalar multiplication.\n", + "# Example usage:\n", + "v = Vector(4, 4)\n", + "print(f\"Vector: {v}\")\n", + "print(f\"Magnitude: {v.magnitude}\")\n", + "print(f\"Direction: {v.direction}°\")\n", + "\n", + "# Add more example usage to test the __mul__ and __add__ methods\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PyGame Vector2\n", + "\n", + "Now that you've done all that work creating your own vector class, we aren't going to use it anymore. Sorry, sometimes you write code you don't use, but we wanted you to really understand vectors. \n", + "\n", + "Instead, we are going to use the Vector class that is built into Pygame: \n", + "\n", + "```python\n", + "from pygame.math import Vector2\n", "\n", - "So, suppose that we have a vector called `p1`, which is the position of the player, at\n", + "```\n", + "\n", + "Suppose that we have a vector called `p1`, which is the position of the player, at\n", "x=100 and y=100. We can write this as:\n", "\n" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -104,7 +242,7 @@ "" ] }, - "execution_count": 7, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -115,7 +253,37 @@ "\n", "p1 = Vector2(100, 100)\n", "\n", - "p1" + "p1 " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When you add vectors, you really just add the x and y values together. " + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[30, 30]\n" + ] + } + ], + "source": [ + "from pygame import Vector2\n", + "\n", + "v1 = Vector2(10, 10 )\n", + "v2 = Vector2(20, 20 )\n", + "\n", + "\n", + "print(v1 + v2) # Same as Vector(10+20, 10+20) = Vector(30, 30)" ] }, { @@ -131,7 +299,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -140,16 +308,17 @@ "" ] }, - "execution_count": 8, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "\n", + "p1 = Vector2(100, 100)\n", "move = Vector2(10, 0) # Move 10 pixels to the right, none up/down\n", "\n", - "p1 += move\n", + "p1 += move # same as: p1 = p1 + move\n", "p1" ] }, @@ -169,7 +338,7 @@ "source": [ "\n", "For a more detaied example. run the program `01a_vector_example.py.` This program has a custom version\n", - "of the vector class that displays on a larger grid. Here is what the programs output looks like:\n", + "of the vector class that displays on a larger grid. Here is what the program's output looks like:\n", "\n", "![Vector Example](images/vector_grid.png)\n", "\n", @@ -191,12 +360,18 @@ "\n", "\n", "## Rotations\n", - "Or, suppose that we want to move the player 100 pixels in the direction of 45 degrees. We can write this as:\n" + "\n", + "Since vectors have a direction, it would make sense that we'd want to change the\n", + "direction. This operation is called a 'rotation'.\n", + "\n", + "Suppose that we want to move the player 100 pixels in the direction of 45\n", + "degrees. The easiest way to write this is to create the vector a 100 units long\n", + "in the x direction, then rotate it to 45 degrees. \n" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -205,14 +380,14 @@ "" ] }, - "execution_count": 9, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "p1 = Vector2(100, 100)\n", - "move = Vector2(100, 0)\n", + "p1 = Vector2(100, 100) # Player's current position\n", + "move = Vector2(100, 0) # 100 unit long vector\n", "move.rotate_ip(45) # rotate the vector \"in-place\"\n", "\n", "p1 += move\n", @@ -224,30 +399,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When you add vectors, you really just add the x and y values together. \n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[30, 30]\n" - ] - } - ], - "source": [ - "from pygame import Vector2\n", - "\n", - "v1 = Vector2(10, 10 )\n", - "v2 = Vector2(20, 20 )\n", + "Notice the `_ip` suffix after `rotate`; this stands for \"in place\" and means we are going to change the vector. Without the `_ip` suffix, the `.rotate()` will return a new vector, so we'd have to write it like this: \n", "\n", - "\n", - "print(v1 + v2)" + "```python\n", + "move = move.rotate()\n", + "```" ] }, { @@ -260,7 +416,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -291,7 +447,12 @@ "\n", "Draw a square with Vectors. \n", "\n", + "For this assignment we will be using our own vector class, `Vector20` which is\n", + "based on `Vector2`, but has extensions for plotting a grid with numbers and\n", + "labels. Use it just like you would a `Vector2`\n", + "\n", "1. Open and read `01a_vector_example.py` and run the program.\n", + "2. Copy the code to a new file, `01b_vector_assignment.py`\n", "2. Create vector v1 with x = 0 and y = 1 \n", "3. Scale the vector by 10 with multiplication, then rotate it by 90 degrees so\n", " it points to the right.\n", @@ -340,7 +501,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.0" + "version": "3.13.3" } }, "nbformat": 4, diff --git a/lessons/03_Vectors/02_Using_Vectors.ipynb b/lessons/03_Vectors/02_Using_Vectors.ipynb index 4c4d827..7a29b24 100644 --- a/lessons/03_Vectors/02_Using_Vectors.ipynb +++ b/lessons/03_Vectors/02_Using_Vectors.ipynb @@ -6,8 +6,6 @@ "source": [ "# Vector Example Program\n", "\n", - "( Use \"Open Preview\" to see the images in this document. )\n", - "\n", "The program `03_vectors.py` is a simple program that demonstrates how\n", "to use vectors to move an object around the screen.\n", "\n",