From d19285496cf7fce88c08c88cfedd768ae1a1a989 Mon Sep 17 00:00:00 2001 From: Eric Busboom Date: Tue, 24 Jun 2025 10:40:37 -0700 Subject: [PATCH 1/3] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index ba5ea2b..93f2c97 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,3 @@ 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. From 17aa2fa7cea97aa1a4a6351884e2fa67f25ef7a1 Mon Sep 17 00:00:00 2001 From: Eric Busboom Date: Fri, 27 Jun 2025 21:16:10 +0000 Subject: [PATCH 2/3] Refactor physics calculations in movement lessons for consistency and clarity --- lessons/01_Physics_for_Games/01_move.py | 36 +++-- .../02_no_acceleration.py | 23 ++- .../01_Physics_for_Games/03_acceleration.py | 20 ++- lessons/01_Physics_for_Games/04_gravity.py | 33 ++-- lessons/01_Physics_for_Games/scratch.ipynb | 143 ++++++++++++++++++ 5 files changed, 217 insertions(+), 38 deletions(-) create mode 100644 lessons/01_Physics_for_Games/scratch.ipynb diff --git a/lessons/01_Physics_for_Games/01_move.py b/lessons/01_Physics_for_Games/01_move.py index ab944bf..fefe326 100644 --- a/lessons/01_Physics_for_Games/01_move.py +++ b/lessons/01_Physics_for_Games/01_move.py @@ -16,9 +16,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,8 +32,8 @@ # 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 @@ -48,19 +51,32 @@ 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 +84,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_Physics_for_Games/02_no_acceleration.py b/lessons/01_Physics_for_Games/02_no_acceleration.py index e3edfb3..010908f 100644 --- a/lessons/01_Physics_for_Games/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_Physics_for_Games/03_acceleration.py b/lessons/01_Physics_for_Games/03_acceleration.py index 73f1591..95b36b5 100644 --- a/lessons/01_Physics_for_Games/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_Physics_for_Games/04_gravity.py b/lessons/01_Physics_for_Games/04_gravity.py index 26ff474..0a96361 100644 --- a/lessons/01_Physics_for_Games/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/scratch.ipynb b/lessons/01_Physics_for_Games/scratch.ipynb new file mode 100644 index 0000000..cf2235a --- /dev/null +++ b/lessons/01_Physics_for_Games/scratch.ipynb @@ -0,0 +1,143 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "9c85ba9f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "1 + 1" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d0e8dc19", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(17.1, 3.0999999999999996)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a = 10.1\n", + "b = 7\n", + "a+b, a-b" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "751a7eea", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(3.1, 3.0999999999999996)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "((a*10) - (b*10))/10, a-b" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "349897fb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "10000000000.0" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a = 10**10\n", + "b = 10**-10\n", + "\n", + "(a+b)-b " + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "102b23fe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "8" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "17//2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de90e7d2", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From c125469b2a8580ccef03a43ae8685b94c15e4092 Mon Sep 17 00:00:00 2001 From: Eric Busboom Date: Fri, 27 Jun 2025 22:11:45 +0000 Subject: [PATCH 3/3] Refactor game settings and player mechanics for improved clarity and consistency --- .../01_Physics_for_Games/05_gravity_bounce.py | 112 ++++++++++-------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/lessons/01_Physics_for_Games/05_gravity_bounce.py b/lessons/01_Physics_for_Games/05_gravity_bounce.py index d176005..15d2166 100644 --- a/lessons/01_Physics_for_Games/05_gravity_bounce.py +++ b/lessons/01_Physics_for_Games/05_gravity_bounce.py @@ -10,37 +10,41 @@ from dataclasses import dataclass @dataclass -class Settings: +class GameSettings: """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 + 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() -# Create an instance of Settings -settings = Settings() +# Initialize game settings +settings = GameSettings() # Initialize screen screen = pygame.display.set_mode((settings.screen_width, settings.screen_height)) +pygame.display.set_caption("Gravity Bounce") -# Define player -player = pygame.Rect(100, settings.screen_height - settings.player_size, settings.player_size, settings.player_size) +# Square starting position +x_pos = 100 +y_pos = settings.screen_height - settings.square_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 +# 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 game loop +# Main loop running = True clock = pygame.time.Clock() @@ -51,56 +55,62 @@ class Settings: if event.type == pygame.QUIT: running = False - # Continuously jump. If the player is not jumping, make it jump + # Continuously jump. If the square 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, + # 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 - player_y_velocity = -settings.jump_y_velocity - player_x_velocity = settings.jump_x_velocity * x_direction + velocity_y = -settings.jump_velocity_y + velocity_x = settings.jump_velocity_x * x_direction is_jumping = True - else: # the player is jumping - # Update player position. Gravity is always pulling the player down, + 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 player go up more slowly. Eventually, the player will have - # a positive y velocity, and gravity will pull the player down. + # to make the square go up more slowly. Eventually, the square will have + # a positive y velocity, and gravity will pull the square down. - player_y_velocity += settings.gravity - player.y += player_y_velocity - player.x += player_x_velocity + velocity_y += settings.gravity * settings.d_t - # 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 + # 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 - # 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 + # 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)) - # Draw everything - screen.fill(settings.white) - pygame.draw.rect(screen, settings.black, player) - + # Update the display pygame.display.flip() - clock.tick(30) + + # Frame rate control + clock.tick(settings.fps) pygame.quit()