├── .gitignore ├── README.md ├── example ├── bouncing_ball │ ├── bouncing_ball.qwq_high.py │ ├── bouncing_ball_qwq_high response.txt │ └── inference_hight_thinking_llamacpp_py.py └── low_thinking │ ├── example_low_thinking_llamacpp_py.py │ └── example_low_thinking_tranformers.py ├── thinking_effort_llamacpp_py.py └── thinking_effort_transformers.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Virtual Environment 2 | .venv/ 3 | venv/ 4 | ENV/ 5 | env/ 6 | 7 | # Python bytecode 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # Distribution / packaging 13 | dist/ 14 | build/ 15 | *.egg-info/ 16 | 17 | # Unit test / coverage reports 18 | htmlcov/ 19 | .tox/ 20 | .coverage 21 | .coverage.* 22 | .cache 23 | nosetests.xml 24 | coverage.xml 25 | *.cover 26 | 27 | # Jupyter Notebook 28 | .ipynb_checkpoints 29 | 30 | # IDE specific files 31 | .idea/ 32 | .vscode/ 33 | *.swp 34 | *.swo 35 | 36 | # OS specific files 37 | .DS_Store 38 | Thumbs.db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thinking Effort Controller 2 | 3 | An experimental approach to controlling the reasoning depth of large language models that use explicit thinking tokens. 4 | 5 | ## Overview 6 | 7 | This repository provides tools to dynamically adjust how much "thinking" a language model does during generation by manipulating the probability of the end-thinking token (``): 8 | 9 | - **Low thinking effort (0.0)**: Model quickly exits the thinking phase 10 | - **Normal thinking effort (1.0)**: No modification to the model's natural behavior 11 | - **High thinking effort (>1.0)**: Model spends more time in the thinking phase 12 | 13 | The `scale_factor` parameter controls the intensity of this effect: 14 | - Higher values (e.g., 4) create a stronger contrast between low and high thinking effort 15 | - The default value (2) works well for many models, but may need adjustment 16 | - The actual scaling applied is calculated as: `scale = scale_factor ^ (1.0 - thinking_effort)` 17 | 18 | This approach works with models trained with explicit reasoning patterns (using tokens like `` and ``), allowing control over reasoning depth without retraining. 19 | 20 | ## Installation 21 | 22 | ### For use with Transformers 23 | 24 | ```bash 25 | pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 26 | pip install transformers 27 | ``` 28 | 29 | ### For use with llama-cpp-python (GGUF quantized models) 30 | 31 | ```bash 32 | pip install llama-cpp-python 33 | ``` 34 | 35 | ## How It Works 36 | 37 | The controller scales the logits (prediction scores) for the end-thinking token based on the desired thinking effort: 38 | 39 | - Scale = scale_factor ^ (1.0 - thinking_effort) 40 | - When thinking_effort = 0, the end token is strongly boosted (less thinking) 41 | - When thinking_effort = 1, no scaling occurs (normal thinking) 42 | - When thinking_effort > 1, the end token is suppressed (more thinking) 43 | 44 | Once the end-thinking token is generated, the controller stops modifying logits. 45 | 46 | ## Important Notes 47 | 48 | - This is an experimental approach - results may vary across models 49 | - You must identify the correct token ID for `` in your specific model 50 | - Different models may require different prompt formats and chat templates 51 | - The `scale_factor` parameter may need adjustment based on the model 52 | 53 | See the example Python files for implementation details and usage patterns. 54 | 55 | ## Running Examples 56 | 57 | ### Bouncing Ball Example 58 | To run the bouncing ball example with llama cpp python: 59 | 60 | ```bash 61 | cd examples/bouncing_ball 62 | python inference_high_thinking_llamacpp.py 63 | ``` 64 | 65 | This example demonstrates the inference for bouncing balls prompt on a high thinking setup (2.5) 66 | 67 | ### Other Examples 68 | Additional examples can be found in the `examples` directory, each showing different use cases for the thinking effort controller. -------------------------------------------------------------------------------- /example/bouncing_ball/bouncing_ball.qwq_high.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import math 3 | import random 4 | from dataclasses import dataclass 5 | import time 6 | 7 | WIDTH, HEIGHT = 800, 800 8 | CANVAS_CENTER = (WIDTH // 2, HEIGHT // 2) 9 | RADIUS_HEPTAGON = 300 # Radius of heptagon vertices from center 10 | BALL_RADIUS = 15 # Radius of each ball 11 | GRAVITY = 100 # Acceleration due to gravity per second 12 | FRICTION = 0.02 # Velocity damping coefficient (per second) 13 | SPIN_FRICTION = 0.01 # Spin damping coefficient (per second) 14 | ELASTICITY = 0.9 # Coefficient of restitution for collisions 15 | 16 | # Colors for balls 17 | COLORS = ['#f8b862', '#f6ad49', '#f39800', '#f08300', '#ec6d51', '#ee7948', '#ed6d3d', '#ec6800', '#ec6800', '#ee7800', 18 | '#eb6238', '#ea5506', '#ea5506', '#eb6101', '#e49e61', '#e45e32', '#e17b34', '#dd7a56', '#db8449', '#d66a35'] 19 | 20 | @dataclass 21 | class Ball: 22 | x: float 23 | y: float 24 | vx: float 25 | vy: float 26 | radius: float 27 | color: str 28 | number: int 29 | spin: float 30 | rotation_angle: float = 0.0 31 | 32 | def main(): 33 | root = tk.Tk() 34 | root.title("Bouncing Balls in Rotating Heptagon") 35 | canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg='white') 36 | canvas.pack() 37 | 38 | # Initialize balls 39 | balls = [] 40 | for i in range(20): 41 | color = COLORS[i % len(COLORS)] 42 | number = i + 1 43 | # Random initial position near center (0,0) 44 | angle = random.uniform(0, 2 * math.pi) 45 | r = random.uniform(0, 30) # Small radius around center 46 | x = r * math.cos(angle) 47 | y = r * math.sin(angle) 48 | vx = 0 49 | vy = 0 50 | balls.append(Ball(x, y, vx, vy, BALL_RADIUS, color, number, 0.0)) 51 | 52 | # Heptagon parameters 53 | num_sides = 7 54 | rotation_angle = 0.0 55 | angular_velocity = 2 * math.pi / 5 # radians per second (360 degrees in 5 seconds) 56 | 57 | # Precompute initial vertices of the heptagon 58 | initial_vertices = [] 59 | angle_step = 2 * math.pi / num_sides 60 | for i in range(num_sides): 61 | angle = i * angle_step 62 | x = RADIUS_HEPTAGON * math.cos(angle) 63 | y = RADIUS_HEPTAGON * math.sin(angle) 64 | initial_vertices.append((x, y)) 65 | 66 | # Variables for timing 67 | last_time = time.time() 68 | 69 | def to_screen(x, y): 70 | return x + CANVAS_CENTER[0], y + CANVAS_CENTER[1] 71 | 72 | def closest_point_on_segment(p, a, b): 73 | ap = (p[0] - a[0], p[1] - a[1]) 74 | ab = (b[0] - a[0], b[1] - a[1]) 75 | dot_ap_ab = ap[0] * ab[0] + ap[1] * ab[1] 76 | dot_ab_ab = ab[0] ** 2 + ab[1] ** 2 77 | if dot_ab_ab == 0: 78 | return a 79 | t = dot_ap_ab / dot_ab_ab 80 | if t < 0: 81 | return a 82 | elif t > 1: 83 | return b 84 | else: 85 | x = a[0] + t * ab[0] 86 | y = a[1] + t * ab[1] 87 | return (x, y) 88 | 89 | def update(): 90 | nonlocal last_time, rotation_angle 91 | current_time = time.time() 92 | delta_time = current_time - last_time 93 | last_time = current_time 94 | 95 | # Update rotation 96 | rotation_angle += angular_velocity * delta_time 97 | rotation_angle %= (2 * math.pi) 98 | 99 | # Compute current polygon vertices once per frame 100 | current_vertices = [] 101 | for (x, y) in initial_vertices: 102 | x_rot = x * math.cos(rotation_angle) - y * math.sin(rotation_angle) 103 | y_rot = x * math.sin(rotation_angle) + y * math.cos(rotation_angle) 104 | current_vertices.append((x_rot, y_rot)) 105 | 106 | edges = [] 107 | for i in range(num_sides): 108 | a = current_vertices[i] 109 | b = current_vertices[(i + 1) % num_sides] 110 | edges.append((a, b)) 111 | 112 | # Update each ball's physics 113 | for ball in balls: 114 | # Apply gravity 115 | ball.vy += GRAVITY * delta_time 116 | 117 | # Apply linear friction (damping) 118 | ball.vx *= (1 - FRICTION * delta_time) 119 | ball.vy *= (1 - FRICTION * delta_time) 120 | 121 | # Update position 122 | ball.x += ball.vx * delta_time 123 | ball.y += ball.vy * delta_time 124 | 125 | # Check for polygon collision 126 | min_distance = float('inf') 127 | closest_edge = None 128 | closest_normal = None 129 | closest_point = None 130 | for edge in edges: 131 | a, b = edge 132 | p = (ball.x, ball.y) 133 | cp = closest_point_on_segment(p, a, b) 134 | distance = math.hypot(cp[0] - ball.x, cp[1] - ball.y) 135 | if distance < min_distance: 136 | min_distance = distance 137 | closest_point = cp 138 | closest_edge = edge 139 | # compute normal vector 140 | dx = b[0] - a[0] 141 | dy = b[1] - a[1] 142 | normal_x = dy 143 | normal_y = -dx 144 | length = math.hypot(normal_x, normal_y) 145 | if length == 0: 146 | normal_unit = (0, 0) 147 | else: 148 | normal_unit = (normal_x / length, normal_y / length) 149 | closest_normal = normal_unit 150 | 151 | if min_distance < ball.radius: 152 | # Resolve collision with polygon edge 153 | normal_unit = closest_normal 154 | # Compute overlap 155 | overlap = ball.radius - min_distance 156 | # Move the ball back along the inward direction 157 | direction = (-normal_unit[0], -normal_unit[1]) 158 | ball.x += direction[0] * overlap 159 | ball.y += direction[1] * overlap 160 | 161 | # Compute velocity reflection 162 | v_dot_n = ball.vx * normal_unit[0] + ball.vy * normal_unit[1] 163 | new_vn = -ELASTICITY * v_dot_n # Reflect with elasticity 164 | 165 | # Tangential components 166 | vt_x = ball.vx - v_dot_n * normal_unit[0] 167 | vt_y = ball.vy - v_dot_n * normal_unit[1] 168 | 169 | # Update velocity 170 | ball.vx = vt_x + new_vn * normal_unit[0] 171 | ball.vy = vt_y + new_vn * normal_unit[1] 172 | 173 | # Update spin based on tangential velocity 174 | # Tangential velocity magnitude 175 | v_tangent = math.hypot(vt_x, vt_y) 176 | ball.spin = v_tangent / ball.radius # Angular velocity 177 | 178 | # Apply spin friction 179 | ball.spin *= (1 - SPIN_FRICTION * delta_time) 180 | ball.rotation_angle += ball.spin * delta_time 181 | 182 | # Check ball-ball collisions 183 | collision_pairs = [] 184 | for i in range(len(balls)): 185 | for j in range(i + 1, len(balls)): 186 | ball1 = balls[i] 187 | ball2 = balls[j] 188 | dx = ball1.x - ball2.x 189 | dy = ball1.y - ball2.y 190 | distance = math.hypot(dx, dy) 191 | if distance < 2 * BALL_RADIUS + 1e-5: 192 | collision_pairs.append((ball1, ball2)) 193 | 194 | # Resolve collisions between balls 195 | for ball1, ball2 in collision_pairs: 196 | dx = ball1.x - ball2.x 197 | dy = ball1.y - ball2.y 198 | distance = math.hypot(dx, dy) 199 | if distance < 1e-5: 200 | continue 201 | nx = dx / distance 202 | ny = dy / distance 203 | rvx = ball1.vx - ball2.vx 204 | rvy = ball1.vy - ball2.vy 205 | dot_product = rvx * nx + rvy * ny 206 | if dot_product > 0: 207 | continue # Moving away, no collision 208 | # Compute impulse 209 | J = (2 * dot_product) / 2 # Both masses same (assuming mass=1) 210 | # Update velocities 211 | ball1.vx -= J * nx 212 | ball1.vy -= J * ny 213 | ball2.vx += J * nx 214 | ball2.vy += J * ny 215 | 216 | # Redraw everything 217 | canvas.delete('all') 218 | # Draw polygon 219 | current_vertices_screen = [to_screen(x, y) for (x, y) in current_vertices] 220 | canvas.create_polygon(current_vertices_screen, outline='black', fill='', width=2) 221 | # Draw balls 222 | for ball in balls: 223 | x, y = to_screen(ball.x, ball.y) 224 | color = ball.color 225 | canvas.create_oval(x - BALL_RADIUS, y - BALL_RADIUS, 226 | x + BALL_RADIUS, y + BALL_RADIUS, fill=color) 227 | # Draw the number rotated by rotation_angle 228 | try: 229 | canvas.create_text(x, y, text=str(ball.number), 230 | fill='black', angle=ball.rotation_angle) 231 | except: 232 | canvas.create_text(x, y, text=str(ball.number), fill='black') 233 | 234 | root.after(16, update) 235 | 236 | root.after(0, update) 237 | root.mainloop() 238 | 239 | if __name__ == '__main__': 240 | main() -------------------------------------------------------------------------------- /example/bouncing_ball/bouncing_ball_qwq_high response.txt: -------------------------------------------------------------------------------- 1 | Okay, I need to create a Python program that displays 20 bouncing balls inside a spinning heptagon. Let me start by breaking down the requirements to understand what I need to do. 2 | 3 | First, the balls must all have the same radius. They need numbers from 1 to 20, and their colors are given in a list. The heptagon spins at 360 degrees every 5 seconds, so that's a rotation speed of 0.72 degrees per frame if the frame rate is 60, but I'll have to calculate the angular velocity properly. 4 | 5 | The balls start by dropping from the center but are randomly distributed. That means when the simulation starts, the balls should be placed in random positions near the center but not overlapping each other. I'll need a function to generate initial positions with some random offsets. 6 | 7 | The balls must be affected by gravity and friction. Bounce off walls realistically, meaning the collisions with the rotating heptagon walls must be calculated properly. Also, ball collisions with each other must be handled. 8 | 9 | The balls should rotate with friction. The numbers on them can indicate spin, so perhaps the number affects the rotation speed? Wait, the problem says "the numbers on the ball can be used to indicate the spin of the ball." Maybe the number is displayed and its rotation is shown through the number's orientation? Hmm, that might be a bit tricky in Tkinter. Alternatively, maybe the spin affects the rotation of the number's text? Not sure yet, but I'll note that. 10 | 11 | The heptagon must be spinning, so each frame, the heptagon's vertices are rotated. The walls are moving, so collisions with moving walls need to be calculated. That complicates things because the walls' positions are changing each frame. So each ball must check for collision with the current polygon edges. 12 | 13 | The heptagon must be large enough to contain all balls. The size should be such that even when the balls are moving, they don't escape. Since the heptagon is rotating, the edges are moving, so the containment is a bit more complex. The balls need to remain inside the rotating polygon. 14 | 15 | I can't use Pygame, so I need to use Tkinter for graphics. Tkinter is a bit slow, but maybe it's manageable for 20 balls. The collision detection and response will be done manually, so I need to implement those algorithms. 16 | 17 | Let me outline the steps I need to take: 18 | 19 | 1. Set up the Tkinter window. 20 | 2. Define the heptagon, which is a 7-sided polygon, rotating around its center. 21 | 3. Define the balls with their properties: position, velocity, radius, color, number, and spin (maybe angular velocity). 22 | 4. Implement the physics loop: update each ball's position based on gravity and friction, handle collisions with the rotating walls, handle collisions between balls, and rotate the heptagon. 23 | 5. Redraw everything each frame. 24 | 25 | First, let's think about the heptagon. To represent it, I need its vertices. Since it's rotating, each frame the vertices are rotated by a certain angle. The rotation angle increases each frame. The initial vertices can be calculated using polar coordinates. Let's say the heptagon has a radius R. The vertices are spaced every 360/7 degrees. The coordinates for each vertex (x,y) can be computed as: 26 | 27 | x = R * cos(angle + initial_angle) 28 | y = R * sin(angle + initial_angle) 29 | 30 | Wait, actually, the initial angle can be set to 0 for simplicity, and then each frame, we add the angular step. The rotation of the heptagon can be represented by a rotation matrix applied to each vertex. 31 | 32 | Wait, but each frame the entire polygon rotates, so each vertex's coordinates are rotated by the current angle. Alternatively, the polygon itself can be considered as rotating around the center, so when checking collisions, the ball's position is transformed into the polygon's rotating frame? 33 | 34 | Hmm, maybe it's easier to precompute the polygon's edges each frame with the current rotation, then check if the ball has collided with any edge. Alternatively, perhaps it's better to represent the polygon edges as a set of line segments, and compute collision between a moving point (ball) and these moving edges. That could be computationally intensive, but for 20 balls, maybe manageable. 35 | 36 | Alternatively, perhaps it's better to rotate the coordinate system so that the polygon is stationary, and the balls are moving in a rotating reference frame. But that might complicate things further. 37 | 38 | Alternatively, for each ball, when checking collision with the polygon, we can first rotate the ball's position by the negative of the polygon's current rotation angle to bring it into a stationary polygon frame. Then perform collision detection with the stationary polygon. That might be easier. 39 | 40 | Yes, that's a good approach. So the steps for collision detection with the polygon would be: 41 | 42 | 1. For each ball, get its current position (x, y) in the global frame. 43 | 2. Rotate this position by -theta, where theta is the current rotation angle of the polygon (since the polygon is rotating, rotating the ball's position in the opposite direction makes the polygon appear stationary in this local frame). 44 | 3. Now, check if the rotated point is outside the polygon. If it is, compute the collision response, then rotate back the corrected position to the global frame. 45 | 46 | Wait, but how does the rotation affect the edges? Hmm, perhaps the polygon's edges can be represented in their initial position (without rotation), and we can rotate the ball's position into the polygon's rotating frame. 47 | 48 | Alternatively, to simplify, the polygon is considered to be rotating, so each edge is moving. Therefore, when a ball moves, if any edge is moving into the ball's position, there could be a collision. But this is complicated. 49 | 50 | Alternatively, let me think of the polygon's edges as static in a rotating frame. But perhaps the first approach is manageable. 51 | 52 | Let me formalize this: 53 | 54 | Assume the polygon is rotating with angular velocity ω radians per second. The rotation angle at time t is θ(t) = ω * t. Each frame, the angle increases by Δθ = ω * Δt, where Δt is the time between frames. 55 | 56 | The polygon's vertices at frame t can be calculated by rotating the initial vertices by θ(t). So the edges of the polygon are between consecutive vertices. 57 | 58 | To detect collision between a ball and the polygon, we need to check if any edge of the polygon intersects with the ball's current position plus its movement vector. Wait, but since the polygon is rotating, the edges are moving, so the ball's trajectory must be checked against moving edges. That complicates things because the edges are moving. 59 | 60 | Alternatively, perhaps the polygon's rotation is slow enough that we can approximate the edges as stationary over a small time step (like one frame). Then, each frame, we can recalculate the polygon's edges with the new rotation angle, and check for collisions between the balls and the new edges. 61 | 62 | So the steps each frame would be: 63 | 64 | 1. Calculate the current rotation angle of the polygon. 65 | 2. Compute the polygon's current vertices by rotating the initial vertices by the current angle. 66 | 3. For each ball: 67 | a. Update velocity based on gravity and friction (since it's affected by gravity). 68 | b. Update position based on velocity. 69 | c. Check for collision with the polygon walls (current edges). 70 | d. Check for collisions with other balls. 71 | e. Apply collision responses (bounce off walls and other balls). 72 | 4. Rotate the polygon's angle by Δθ. 73 | 5. Redraw all balls and polygon. 74 | 75 | Now, the main challenges are: 76 | 77 | - Implementing collision detection between a moving ball and the polygon edges. Since the polygon is rotating, but we are checking each frame as a static polygon, maybe the movement during the frame can be approximated as instantaneous for collision detection. 78 | 79 | Wait, but the ball is moving in a straight line during the frame (assuming velocity is constant during the frame). So for each ball, we can check if during its movement from previous position to new position, it would intersect with any edge of the polygon. However, since the edges are also moving (because the polygon is rotating), this complicates things. 80 | 81 | Alternatively, perhaps the rotation of the polygon is slow enough that we can treat the polygon as stationary during a single frame. So each frame, the polygon is in a new position, and the ball's new position is calculated, then collisions are checked with the polygon in its new position. 82 | 83 | This approach would require that the angular velocity is small enough that the polygon doesn't move too much between frames. Since the rotation is 360 degrees over 5 seconds, that's 0.72 degrees per second, so for a frame rate of, say, 60 FPS, each frame is 1/60 seconds, so the angular step per frame is 0.72 / 60 ≈ 0.012 degrees. That's very small, so the polygon moves very slowly. So the polygon can be considered stationary during each frame, and the ball's movement can be checked against the polygon's current edges. So this approach is feasible. 84 | 85 | So the process is: 86 | 87 | Each frame: 88 | 89 | 1. Update polygon rotation angle by Δθ (calculated as (2π radians)/(5 seconds) * Δt, where Δt is the time since last frame). 90 | 91 | 2. For each ball: 92 | 93 | a. Compute new position based on current velocity (affected by gravity and friction). 94 | 95 | b. Check if the new position is inside the polygon. If not, compute the collision point and adjust the position and velocity accordingly. 96 | 97 | Wait, but how to check if the ball is inside the polygon? Alternatively, check if the ball's center is inside the polygon, and also check for collision with edges. 98 | 99 | Wait, the edges of the polygon are moving (due to rotation), so between frames, the polygon has rotated a tiny bit. The ball's path during the frame could cross a moving edge. But since the rotation is small, maybe just checking the current position against the polygon in its new rotated position is sufficient. 100 | 101 | Alternatively, perhaps we can treat the polygon as rotating continuously, and model the collision as the ball colliding with a rotating wall. But that's more complex. 102 | 103 | Hmm, perhaps the simplest way is to compute the polygon's current edges each frame, and then for each ball, check its current position against the polygon's edges. If the ball's center is outside the polygon, then we need to find the closest edge and compute the collision response. 104 | 105 | Wait, but the polygon's edges are the boundaries. So the ball's center must remain within the polygon. So when the ball's center is outside, we need to move it back inside and reverse the velocity component perpendicular to the edge. 106 | 107 | Alternatively, perhaps the polygon is convex, so we can use point-in-polygon tests. If the ball's center is outside, then it's hit a wall. To find which edge it hit, we can compute the distance from the point to each edge and find the closest edge. 108 | 109 | Alternatively, perhaps using the separating axis theorem (SAT) for collision between the ball and the polygon. Since the polygon is convex, SAT can be used. But that might be computationally intensive for each frame and each ball. 110 | 111 | Alternatively, perhaps for each edge of the polygon, we can compute the distance from the ball's center to the edge. If the distance is less than the ball's radius, then it's colliding with that edge. The direction of the normal can be used to reflect the velocity. 112 | 113 | This approach would require for each edge: 114 | 115 | - Represent the edge as a line segment between two vertices. 116 | 117 | - Compute the distance from the ball's center to the edge. If less than radius, then collision. 118 | 119 | Additionally, if the ball's center is outside the polygon (i.e., the distance to all edges is more than radius, but the center is outside the polygon), then it's also considered a collision. 120 | 121 | Hmm, perhaps the first step is to check whether the ball's center is inside the polygon. If not, then it's outside, so we need to find the nearest edge and compute the collision. 122 | 123 | Alternatively, the polygon is a convex polygon, so the ball is outside if any of its edges are closer than the ball's radius, but perhaps this is getting complicated. 124 | 125 | Alternatively, let me think of the edges as lines. For each edge: 126 | 127 | - The edge is a line segment between two vertices. 128 | 129 | - The distance from the ball's center to the edge's line. 130 | 131 | - If the distance is less than the ball's radius, then the ball is colliding with that edge. 132 | 133 | But also, the ball could be outside the polygon, so the edge must be the one that the ball is on the 'outside' side of. 134 | 135 | Alternatively, perhaps a better way is to represent the polygon as a list of edges (each as a line segment), and for each edge, compute the distance from the ball's center to the edge. The closest edge will determine the collision. 136 | 137 | Wait, but for a convex polygon, the closest edge to the center outside the polygon is the one that the ball has collided with. 138 | 139 | Alternatively, here's a possible approach: 140 | 141 | For each ball: 142 | 143 | - Check if the center is inside the polygon. If yes, no collision with walls. 144 | 145 | - If not, compute the distance to each edge and find the closest edge. The distance must be less than the ball's radius plus some tolerance (since the ball's size is considered). 146 | 147 | Wait, but the edges are the walls, so the ball's center can't be within the polygon but the ball is outside due to radius? No, the ball's radius is part of its size, so the center must be at least radius away from the edge. 148 | 149 | Hmm, maybe I need to compute for each edge whether the ball's center is outside the polygon and within the edge's proximity (distance less than radius). 150 | 151 | Alternatively, perhaps the best way is to represent the polygon as a convex shape and use the separating axis theorem for collision between the ball (as a circle) and the polygon. 152 | 153 | The SAT says that if there exists an axis along which the projections of the two shapes do not overlap, then they are not colliding. For a circle and a convex polygon, you can check all edges of the polygon and the circle's center. 154 | 155 | Alternatively, here's a method for circle-polygon collision detection: 156 | 157 | 1. For each edge of the polygon: 158 | 159 | a. Treat the edge as a line segment. 160 | 161 | b. Find the closest point on the edge to the circle's center. 162 | 163 | c. Calculate the distance between the center and this closest point. 164 | 165 | d. If this distance is less than the circle's radius, then there's a collision. 166 | 167 | e. Also, the closest point must lie on the edge (i.e., between the two vertices). 168 | 169 | 2. Additionally, check whether the center is inside the polygon (if so, no collision with edges). 170 | 171 | Wait, but if the center is inside the polygon, then the ball is inside, so no collision. If the center is outside, then the ball's edge may be outside the polygon, so we need to find which edge it's colliding with. 172 | 173 | Therefore, the steps could be: 174 | 175 | For each ball: 176 | 177 | - Compute the center (x,y). 178 | 179 | - Check if the center is inside the polygon: 180 | 181 | a. If yes: then check the distance from the center to each edge. If any edge is closer than the radius, then it's a collision (this can happen if the ball is too large, but according to the problem, the heptagon is large enough). 182 | 183 | Wait, the problem says the heptagon is large enough to contain all balls. So perhaps the center being inside the polygon is sufficient, and collisions with edges only occur when the ball's radius extends beyond the edge. 184 | 185 | Hmm, maybe I'm overcomplicating. Let's think of the polygon as the boundary. The ball must remain entirely within the polygon. The ball's center must be at least radius away from all edges. 186 | 187 | Alternatively, the ball's center can be inside the polygon, but if it gets too close to an edge (distance < radius), then it must bounce back. 188 | 189 | Wait, the problem states that the balls should remain inside the heptagon. So when a ball's center is moving towards an edge such that the distance becomes less than the radius, then the ball is considered to have collided with the edge. 190 | 191 | So the algorithm would be: 192 | 193 | For each ball: 194 | 195 | - Compute new position based on velocity (with gravity/friction applied). 196 | 197 | - Check if the new position is inside the polygon. 198 | 199 | - If not, the ball has gone outside, so need to find the closest edge and compute collision response. 200 | 201 | - If yes, check the distance to each edge. If any edge's distance to the ball's center is less than radius, then it's colliding with that edge. 202 | 203 | Wait, perhaps the first check is whether the center is inside the polygon. If it is, then check the distance to edges to see if any are within the radius. If none, then no collision. If any edge is within radius, then collision with that edge. 204 | 205 | Alternatively, maybe the edge distance must be <= radius for collision. 206 | 207 | Hmm, perhaps the best way is to compute for each edge the distance from the center to the edge's line, then clamp the center position back to the polygon if it's outside, and adjust the velocity accordingly. 208 | 209 | Alternatively, perhaps it's easier to first check if the ball is outside the polygon. To do that, we can use the point-in-polygon test. 210 | 211 | The point-in-polygon test can be done by counting the number of edges that the ray from the point in any direction crosses. For convex polygons, a simpler method is to check the cross product with each edge. 212 | 213 | For a convex polygon, the point is inside if it is on the "inside" side of all edges. 214 | 215 | So for each edge of the polygon (defined by vertices A and B), the edge can be considered as a vector from A to B, and the point P must be on the correct side of each edge. 216 | 217 | Alternatively, here's a method for convex polygon point-inclusion: 218 | 219 | For each edge of the polygon (defined by two consecutive vertices v_i and v_{i+1}), compute the cross product (v_{i+1} - v_i) × (P - v_i). The sign of this cross product must be consistent for all edges (all positive or all negative) for the point to be inside. 220 | 221 | Wait, actually, for convex polygons, you can compute the cross product with the edge's normal direction. Let me think again. 222 | 223 | The standard approach for convex polygon is: 224 | 225 | - For each edge, compute the cross product of the edge's direction with the vector from the edge's starting point to the point. The sign of this cross product must be consistent for all edges (all positive or all negative) for the point to be inside. 226 | 227 | Wait, perhaps a better way is to compute the winding number, but for convex polygons, there's a simpler approach. 228 | 229 | Alternatively, here's a step-by-step method: 230 | 231 | For each edge of the polygon (from vertex i to vertex i+1): 232 | 233 | 1. Compute the direction vector of the edge: d = (v_i+1.x - v_i.x, v_i+1.y - v_i.y) 234 | 235 | 2. Compute the vector from v_i to the point P: w = (P.x - v_i.x, P.y - v_i.y) 236 | 237 | 3. The cross product of d and w is d.x * w.y - d.y * w.x. 238 | 239 | 4. The sign of this cross product determines which side of the edge the point is on. 240 | 241 | If all cross products have the same sign (all positive or all negative), then the point is inside the polygon. 242 | 243 | Wait, actually, for convex polygons, the point is inside if it is on the "same side" (as defined by the polygon's winding order) of all edges. 244 | 245 | So, choosing a consistent winding order (e.g., clockwise or counter-clockwise), the cross product signs must all be positive (or all negative) for the point to be inside. 246 | 247 | This requires that all edges are ordered consistently (either all clockwise or all counter-clockwise). 248 | 249 | So, first, I need to ensure that the polygon vertices are ordered in a consistent winding order (e.g., clockwise). 250 | 251 | Assuming the polygon is generated in clockwise order, then for the point to be inside, all cross products must be positive (or negative, depending on the direction). 252 | 253 | Wait, perhaps I need to think of the edges as vectors, and the cross product's sign tells whether the point is on the left or right side of the edge. 254 | 255 | Alternatively, perhaps it's better to compute the sign and check that all are positive (assuming edges are ordered in a way that the interior is on the left side of each edge). 256 | 257 | Hmm, perhaps I need to implement this point-in-polygon test first. 258 | 259 | Once I know whether the point is inside, I can proceed. 260 | 261 | If the point is outside, then I need to find which edge it's closest to, and compute the collision response. 262 | 263 | Alternatively, even if inside, the distance to the edge might be less than the radius, so the ball is touching the edge. 264 | 265 | Hmm, perhaps for the purpose of collision detection, I need to: 266 | 267 | For each edge of the polygon: 268 | 269 | a. Compute the distance from the ball's center to the edge. 270 | 271 | b. If the distance is less than the radius, then the ball is colliding with that edge. 272 | 273 | c. Also, the point must be on the 'outside' side of the polygon (since the polygon's edges are the boundary, so the collision is only when the center is approaching from inside towards outside, beyond the edge). 274 | 275 | Wait, this is getting complicated. Maybe an alternative approach: 276 | 277 | The ball is considered to have collided with the polygon if the distance from its center to any edge is less than the radius, and the direction of the velocity is towards the edge. 278 | 279 | Alternatively, perhaps the simplest way for this problem is to compute for each edge the distance from the ball's center to the edge. If any edge has a distance less than the ball's radius, then a collision with that edge has occurred. The normal vector of that edge is used to compute the reflection. 280 | 281 | But I need to ensure that the point is outside the polygon. Wait, the distance can be computed as signed distance (positive inside, negative outside?), which might help. 282 | 283 | Alternatively, here's a plan for collision with polygon walls: 284 | 285 | 1. For the current polygon rotation angle, compute all edges as line segments. 286 | 287 | 2. For each edge (segment between vertices v1 and v2): 288 | 289 | a. Find the closest point on the edge to the ball's center (x,y). 290 | 291 | b. Compute the distance between (x,y) and this closest point. 292 | 293 | c. If this distance < radius, and the ball's center is outside the polygon, then collision with this edge. 294 | 295 | But how to compute the closest point on the edge? 296 | 297 | The closest point can be found by projecting the center onto the line of the edge, then clamping to the segment. 298 | 299 | Alternatively, here's a method to compute the closest point on a line segment: 300 | 301 | Given a line segment from point A to B, and a point P: 302 | 303 | vector AP = P - A 304 | 305 | vector AB = B - A 306 | 307 | t = (AP · AB) / (AB · AB) 308 | 309 | if t < 0, closest point is A 310 | 311 | if t > 1, closest point is B 312 | 313 | else, closest point is A + t*AB 314 | 315 | Then the distance from P to this closest point is the distance between P and the closest point. 316 | 317 | Once we have this, we can check whether this distance is less than the radius. 318 | 319 | Also, need to check if the point is on the 'outside' side of the polygon. 320 | 321 | Hmm, perhaps the signed distance would help. For each edge: 322 | 323 | Compute the normal vector pointing outward from the polygon. The distance can be signed such that negative values mean the point is outside. 324 | 325 | Wait, perhaps for each edge, the normal vector is pointing outward. The distance from the center to the edge along the normal direction must be less than the radius. 326 | 327 | Alternatively, perhaps the following steps for each edge: 328 | 329 | - The edge is between vertices v1 and v2. 330 | 331 | - Compute the direction vector of the edge: AB = (v2.x - v1.x, v2.y - v1.y) 332 | 333 | - The normal vector pointing outward can be computed as (AB.y, -AB.x) (rotated 90 degrees clockwise). Wait, depends on the winding order. 334 | 335 | Alternatively, perhaps the normal is pointing to the outside of the polygon. To compute that, assuming the polygon is ordered clockwise, the outward normal can be (AB.y, -AB.x). 336 | 337 | Wait, if the edge is part of a clockwise polygon, then the outward normal would be the direction pointing to the right of the edge's direction. 338 | 339 | Alternatively, perhaps I need to compute the outward normal for each edge. 340 | 341 | Once we have the outward normal, the distance from the center to the edge along that normal direction must be less than the radius for collision. 342 | 343 | Wait, but how does that work? 344 | 345 | Alternatively, perhaps the signed distance can be computed by projecting the vector from v1 to P onto the normal direction. 346 | 347 | The formula for signed distance to a line: 348 | 349 | The line equation can be written as ax + by + c = 0. 350 | 351 | The signed distance is (ax + by + c)/sqrt(a² + b²). 352 | 353 | The normal vector (a,b) points in the direction of the normal. 354 | 355 | So for each edge, compute its line equation, then compute the signed distance from the ball's center. If the absolute value is less than radius, and the sign indicates it's outside, then collision. 356 | 357 | Hmm, perhaps this is a better approach. 358 | 359 | First, for each edge (v1, v2): 360 | 361 | Compute the line equation ax + by + c = 0. 362 | 363 | The normal vector (a,b) points outward. 364 | 365 | The line can be defined as follows: 366 | 367 | The direction vector of the edge is (dx, dy) = (v2.x - v1.x, v2.y - v1.y) 368 | 369 | The normal vector can be (-dy, dx) (rotated 90 degrees counter-clockwise), which would point outward if the polygon is clockwise. 370 | 371 | Wait, let's see: 372 | 373 | If the polygon is ordered clockwise, then the outward normal would be to the right of the edge direction. 374 | 375 | The direction of the edge is from v1 to v2. The normal pointing outward would be (dy, -dx) ? 376 | 377 | Wait, perhaps the normal vector should be perpendicular to the edge direction, pointing outward. 378 | 379 | Let me think of an edge going from (0,0) to (1,0). The edge direction is (1,0). The outward normal (assuming clockwise winding) would be (0,-1), pointing downward. 380 | 381 | Wait, if the polygon is a square with vertices ordered clockwise, the edge from (0,0) to (1,0) would have a normal pointing down (since the interior is on the right side of the edge direction). 382 | 383 | So the normal vector for that edge would be (0,-1). 384 | 385 | The edge direction is (1,0). The perpendicular direction is (0, -1) (if we rotate the edge direction 270 degrees, or 90 degrees clockwise). 386 | 387 | Alternatively, rotating the direction vector 90 degrees clockwise gives (0,-1), which is the outward normal for a clockwise polygon. 388 | 389 | Yes, so: 390 | 391 | Given edge direction (dx, dy), the outward normal is (dy, -dx). 392 | 393 | Wait, let's compute: 394 | 395 | edge direction vector is (dx, dy) = (v2.x - v1.x, v2.y - v1.y) 396 | 397 | Then the outward normal vector (for clockwise polygon) is (dy, -dx). Because if the direction is (1,0), then normal is (0, -1), which is downward, as expected. 398 | 399 | So the normal vector is (dy, -dx). To make it a unit vector, divide by its magnitude, but for distance purposes, we can keep it as is for the equation. 400 | 401 | The line equation can be written as: 402 | 403 | Normal.x*(x - v1.x) + Normal.y*(y - v1.y) = 0 404 | 405 | Wait, the normal vector (a,b) is perpendicular to the edge, and the line equation is a(x - v1.x) + b(y - v1.y) = 0. 406 | 407 | So: 408 | 409 | a = dy 410 | 411 | b = -dx 412 | 413 | c = -a*v1.x - b*v1.y 414 | 415 | Wait, expanding the line equation: 416 | 417 | a(x - v1.x) + b(y - v1.y) = 0 → ax + by - (a*v1.x + b*v1.y) = 0 → ax + by + c =0 where c = - (a*v1.x + b*v1.y) 418 | 419 | Thus, the signed distance from point (x_p, y_p) to this line is: 420 | 421 | d = (a*x_p + b*y_p + c) / sqrt(a² + b²) 422 | 423 | The sign tells us which side of the line the point is on. If the outward normal points outward, then negative distances mean the point is inside the polygon? Or the other way around? 424 | 425 | Wait, let's see. For the example edge from (0,0) to (1,0): 426 | 427 | Normal vector is (0, -1). 428 | 429 | Line equation: 0*(x-0) + (-1)*(y-0) = 0 → -y = 0 → y = 0. 430 | 431 | The signed distance for a point (0.5, 1): 432 | 433 | d = (0*0.5 + (-1)*1 + 0)/sqrt(0 + 1) → (-1)/1 = -1. So the distance is -1, but the point is above the edge (since y=1 is above the edge at y=0). Since the normal is pointing downward (negative y direction), a positive distance would mean in the direction of the normal (downwards), so a negative distance means above the edge. 434 | 435 | Hmm, so the sign of the distance indicates the side relative to the normal direction. 436 | 437 | Thus, if the polygon is clockwise, the outward normal points outward, so the distance being negative would mean the point is on the inside of the polygon (since the normal points outward, so the distance is measured in the normal's direction). 438 | 439 | Wait, perhaps the distance formula's sign is such that positive means on the normal's direction, negative the opposite. 440 | 441 | So, for a point inside the polygon, the signed distance would be negative (since the normal points outward, so the distance is negative meaning the point is on the side opposite the normal). 442 | 443 | Therefore, for collision with the edge, we need the signed distance to be less than the ball's radius in absolute value, but the point must be outside the polygon (i.e., the signed distance is negative? Or positive?) 444 | 445 | Wait, if the point is inside the polygon, then the signed distance to the edge is negative (because the normal points outward), so the absolute distance is |distance|. 446 | 447 | Wait, this is getting confusing. Let me think of it this way: 448 | 449 | For collision with the edge, the ball's center must be within radius distance from the edge, and the direction is such that the ball is moving outside the polygon. 450 | 451 | Alternatively, perhaps the following conditions must be met for a collision: 452 | 453 | The absolute value of the signed distance is less than the ball's radius, and the point is outside the polygon (i.e., the signed distance is positive? Because the normal points outward, so a positive distance would mean the point is on the outside side of the edge). 454 | 455 | Wait, in the previous example, if the edge is part of a clockwise polygon, and the normal points outward (downward in the example edge), then a point above the edge (y=1) would have a signed distance of -1 (because the normal is pointing down, and the distance is measured in that direction). So the distance is negative, meaning the point is on the inside side of the edge? 456 | 457 | Hmm, maybe I need to reverse the normal direction. 458 | 459 | Alternatively, perhaps I should compute the inward normal. 460 | 461 | Alternatively, perhaps the key is to compute the signed distance and the point's position relative to the polygon. 462 | 463 | Alternatively, perhaps for collision purposes, we can ignore the polygon's inside/outside and just check if the distance to any edge is less than the radius, and the velocity's direction is towards the edge. 464 | 465 | But perhaps that's too much. 466 | 467 | Alternatively, let me try to proceed step by step. 468 | 469 | First, implement the polygon rotation and edges. 470 | 471 | The heptagon has 7 vertices. Let's compute the initial vertices. 472 | 473 | The initial vertices can be computed using polar coordinates. Let's set the center of the polygon at (0,0) for simplicity, and then translate to the screen coordinates later. 474 | 475 | The polygon has a radius R (distance from center to vertices). Let's choose R such that all balls can move inside. The balls have a radius r. The heptagon must be large enough so that 2r < R (so the balls can move without overlapping the edges when centered). 476 | 477 | The vertices are placed at angles 0, 2π/7, 4π/7, etc. 478 | 479 | Wait, but since the polygon is rotating, we can represent it with vertices in a list, then each frame rotate them. 480 | 481 | Alternatively, precompute the vertices once, then rotate each vertex each frame by the current angle. 482 | 483 | Wait, here's the plan for the polygon: 484 | 485 | - The polygon has a center at (0,0) initially. 486 | 487 | - The initial vertices are computed as: 488 | 489 | vertices = [] 490 | for i in range(7): 491 | angle = 2 * pi * i /7 + initial_angle 492 | x = R * cos(angle) 493 | y = R * sin(angle) 494 | vertices.append( (x,y) ) 495 | 496 | But since it's rotating, each frame the angle is added by delta_theta. 497 | 498 | Alternatively, each vertex's angle is their initial angle plus the current rotation angle theta. 499 | 500 | Wait, perhaps it's better to compute each vertex's position each frame by rotating their initial angle by theta. 501 | 502 | Wait, perhaps the vertices are stored as their initial angles, then each frame, their positions are computed as: 503 | 504 | for each vertex in initial_vertices: 505 | x = R * cos(vertex_angle + theta) 506 | y = R * sin(vertex_angle + theta) 507 | 508 | But this requires storing the initial angles. 509 | 510 | Alternatively, the initial vertices are stored as coordinates relative to the center, and each frame, the entire polygon is rotated by theta. 511 | 512 | Rotation can be done using rotation matrices. 513 | 514 | The rotation matrix is: 515 | 516 | x' = x*cos(theta) - y*sin(theta) 517 | 518 | y' = x*sin(theta) + y*cos(theta) 519 | 520 | So for each vertex (x,y) in the initial vertex list, the rotated position is (x_rot, y_rot) computed via rotation. 521 | 522 | Wait, but the initial vertices are stored in their initial positions, and each frame, we compute the rotated vertices by applying the current rotation angle. 523 | 524 | Yes, that's manageable. 525 | 526 | So the steps to compute the polygon's current vertices are: 527 | 528 | current_vertices = [] 529 | theta = current_rotation_angle # which increases each frame 530 | for (x,y) in initial_vertices: 531 | x_rot = x * cos(theta) - y * sin(theta) 532 | y_rot = x * sin(theta) + y * cos(theta) 533 | current_vertices.append( (x_rot, y_rot) ) 534 | 535 | Then, the edges are between consecutive vertices, and the last vertex connects back to the first. 536 | 537 | Now, for collision detection between the ball and the polygon: 538 | 539 | First, for the ball's current position (x,y): 540 | 541 | 1. Check if the point is inside the polygon. If not, then it must be outside, so we need to find the closest edge. 542 | 543 | 2. If inside, check if any edge is within radius distance. 544 | 545 | Alternatively, compute for each edge the distance from the ball's center to the edge. If any edge's distance is less than radius, then collision. 546 | 547 | But how to compute the distance. 548 | 549 | Alternatively, for each edge (v1, v2): 550 | 551 | a. Compute the closest point on the edge to the ball's center (x,y). 552 | 553 | b. The distance between (x,y) and that point is d. 554 | 555 | c. If d < radius, then collision. 556 | 557 | d. Also, the closest point must be on the edge segment. 558 | 559 | But how to compute the closest point. 560 | 561 | Here's a function to find the closest point on a line segment to a point: 562 | 563 | def closest_point_on_segment(p, a, b): 564 | # p is the point (x,y) 565 | # a and b are the endpoints of the segment 566 | ap = (p[0] - a[0], p[1] - a[1]) 567 | ab = (b[0] - a[0], b[1] - a[1]) 568 | 569 | # Compute the projection of ap onto ab 570 | dot_ap_ab = ap[0]*ab[0] + ap[1]*ab[1] 571 | dot_ab_ab = ab[0]*ab[0] + ab[1]*ab[1] 572 | 573 | if dot_ab_ab == 0: 574 | # a and b are the same point, so closest is a 575 | return a 576 | 577 | t = dot_ap_ab / dot_ab_ab 578 | 579 | if t < 0: 580 | return a 581 | elif t > 1: 582 | return b 583 | else: 584 | return (a[0] + t*ab[0], a[1] + t*ab[1]) 585 | 586 | Then, the distance between p and this point is the distance from p to the closest point. 587 | 588 | Once we have that, if distance < radius, then collision with this edge. 589 | 590 | Then, among all edges, find the edge with the smallest distance (closet edge), and compute the collision with that edge. 591 | 592 | The collision response would involve: 593 | 594 | - Moving the ball's center back so that it's exactly radius distance away from the edge along the normal direction. 595 | 596 | - Reversing the velocity component perpendicular to the edge. 597 | 598 | But the direction of the normal is important here. 599 | 600 | Wait, to compute the normal direction pointing outward from the polygon. 601 | 602 | As before, for each edge between a and b: 603 | 604 | edge direction vector is (b.x - a.x, b.y - a.y) 605 | 606 | normal vector outward is (dy, -dx), where dx = b.x -a.x, dy = b.y -a.y. 607 | 608 | Wait, let me confirm: 609 | 610 | edge direction is (dx, dy) = (b.x - a.x, b.y - a.y) 611 | 612 | Normal outward vector is (dy, -dx) ? 613 | 614 | Wait, in the example where edge is from (0,0) to (1,0): 615 | 616 | dx = 1, dy = 0. 617 | 618 | Normal vector is (0, -1), which points downward, outward from the polygon (if polygon is clockwise). 619 | 620 | Yes, so that seems right. 621 | 622 | Thus, the normal vector is (dy, -dx). 623 | 624 | To get a unit normal vector: 625 | 626 | length = sqrt(dy**2 + (-dx)**2) = sqrt(dx² + dy²) 627 | 628 | unit_normal = (dy / length, -dx / length) 629 | 630 | Wait, no, the normal vector is (dy, -dx), so its length is sqrt(dy² + dx²), which is the same as the edge length. 631 | 632 | Thus, unit_normal is ( dy / edge_length , -dx / edge_length ) 633 | 634 | Once we have the unit normal, we can compute the collision response. 635 | 636 | When the ball collides with the edge, the velocity is adjusted to reverse the component along the normal direction. 637 | 638 | Assuming a perfectly elastic collision (coefficient of restitution = 1), the velocity perpendicular to the normal is reversed. 639 | 640 | The formula is: 641 | 642 | v' = v - 2 * (v · n) * n 643 | 644 | where n is the unit normal vector pointing outward from the polygon. 645 | 646 | Wait, but if the ball is moving towards the edge (i.e., the velocity component along the normal is negative, because the normal points outward), then reversing that component would make it move away. 647 | 648 | Wait, let me think: 649 | 650 | Suppose the normal points outward. The velocity's component along the normal direction is v · n. 651 | 652 | If the ball is approaching the edge (velocity towards the edge), then the dot product would be negative (since the normal is outward, and velocity direction is opposite). 653 | 654 | Wait, let's say the normal points outward (away from polygon center). The velocity is towards the edge (so direction towards the normal's direction), so v · n would be positive. 655 | 656 | Wait, maybe I'm confused here. 657 | 658 | Suppose the ball is moving towards the edge (i.e., towards the polygon's boundary). The normal points outward from the polygon. So the direction of the velocity towards the edge is in the direction of the normal. Thus, the velocity's component along the normal is positive. So the dot product would be positive. 659 | 660 | Thus, the new velocity would be v - 2*(v · n)*n, which flips the normal component. 661 | 662 | Yes, so that would reverse the direction away from the edge. 663 | 664 | Additionally, the ball's position might need to be adjusted to prevent penetration. So if the distance to the edge is less than radius, we can move the center along the normal direction by (radius - distance). 665 | 666 | Wait, the distance between the center and the edge is d. So if d < radius, the overlap is (radius - d). To push the ball back, we move it along the normal direction by (radius - d). 667 | 668 | Wait, but the normal direction is outward. So the center is inside the polygon, but the edge is the boundary. Wait, perhaps I need to move in the direction opposite to the normal (since the normal points outward, and the ball is outside the polygon? Or inside? 669 | 670 | Hmm, this is getting too tangled. Let's try to structure the steps: 671 | 672 | For each ball: 673 | 674 | 1. Compute new position based on velocity and delta time (with gravity and friction applied). 675 | 676 | 2. Check all edges of the polygon to find the closest edge that the ball is colliding with (distance < radius). 677 | 678 | 3. If any collision found: 679 | 680 | a. Compute the normal vector pointing outward from the polygon at that edge. 681 | 682 | b. Compute the overlap distance (radius - distance). 683 | 684 | c. Move the ball's center along the normal direction by the overlap distance to prevent penetration. 685 | 686 | d. Compute the reflection of the velocity vector off the normal. 687 | 688 | e. Apply friction to the velocity (maybe reduce the velocity's magnitude slightly). 689 | 690 | Wait, but step 3a requires knowing the edge's normal direction. 691 | 692 | Alternatively, when computing the closest point on the edge, we can also compute the normal direction. 693 | 694 | Alternatively, when we loop through edges to find the closest one, we can also track the normal and closest point. 695 | 696 | This is going to be computationally intensive, but manageable for 20 balls and 7 edges. 697 | 698 | Now, for the ball to ball collisions: 699 | 700 | Each ball must check with every other ball to see if their centers are closer than the sum of their radii (assuming all balls have the same radius). 701 | 702 | If so, compute the collision response. 703 | 704 | The collision between two balls can be resolved by calculating the normal vector between their centers, then reflecting their velocities. 705 | 706 | The steps for ball-ball collision: 707 | 708 | For each pair of balls (ball1, ball2): 709 | 710 | if distance between centers < 2*r: 711 | 712 | compute the normal vector (ball2.pos - ball1.pos) / distance 713 | 714 | compute the relative velocity (ball1.vel - ball2.vel) · normal (dot product) 715 | 716 | if the relative velocity is positive (meaning they are moving away from each other), no collision. 717 | 718 | else: 719 | 720 | compute the impulse and adjust velocities. 721 | 722 | Wait, the exact collision response requires some calculations. 723 | 724 | The standard method for elastic collision between two balls with the same mass (assuming all balls have the same mass): 725 | 726 | The velocity change for each ball is calculated as: 727 | 728 | v1' = v1 - ( ( (v1 - v2) · e ) * (n) ) * 2 729 | 730 | v2' = v2 + ( ( (v1 - v2) · e ) * (n) ) * 2 731 | 732 | where e is the coefficient of restitution (1 for elastic), and n is the unit vector between centers. 733 | 734 | Alternatively, the formula can be written as: 735 | 736 | delta_v1 = - ( ( (v1 - v2) · n ) ) * n * 2 737 | 738 | delta_v2 = ( ( (v1 - v2) · n ) ) * n * 2 739 | 740 | Wait, perhaps it's better to look up the formula. 741 | 742 | The velocity correction formula for elastic collision between two balls of equal mass: 743 | 744 | Let’s denote: 745 | 746 | - m1 = m2 = m (masses are equal) 747 | 748 | - v1 and v2 are velocities before collision 749 | 750 | - r1 and r2 are positions 751 | 752 | The normal vector n is (r2 - r1) / |r2 - r1| 753 | 754 | The relative velocity along n is (v1 - v2) · n 755 | 756 | The change in velocities are: 757 | 758 | v1’ = v1 - ( ( (v1 - v2) · n ) * n ) * 2 759 | 760 | v2’ = v2 + ( ( (v1 - v2) · n ) * n ) * 2 761 | 762 | This will reverse their velocities along the normal direction. 763 | 764 | So that's the formula to use. 765 | 766 | Now, implementing this requires checking all pairs of balls, which is O(n^2) for 20 balls, which is manageable. 767 | 768 | Now, the main steps for the program: 769 | 770 | Initialize the window with Tkinter. 771 | 772 | Define the heptagon's parameters: 773 | 774 | - Radius (R), which should be large enough to contain the balls. Let's say the balls have radius r. So R should be, say, 2*r multiplied by sqrt(2) or something, but perhaps just set R = 100 units, and r = 10. Then the heptagon can be centered at (0,0), and the initial positions of the balls are scattered near the center. 775 | 776 | Wait, but in Tkinter, the coordinates are screen coordinates. We need to map the world coordinates to the screen. 777 | 778 | Let me plan the coordinate system: 779 | 780 | - The center of the heptagon is at (0,0) in world coordinates. 781 | 782 | - The screen has a canvas with width and height, say 800x800. The center of the canvas is at (400,400), so we can map world coordinates to screen by adding 400 to both x and y. 783 | 784 | Thus: 785 | 786 | def to_screen(x, y): 787 | 788 | return (x + 400, y + 400) 789 | 790 | Now, the heptagon's vertices are computed with their coordinates, then converted to screen coordinates for drawing. 791 | 792 | The balls are represented as objects with position (x,y), velocity (vx,vy), radius, color, number, and spin (maybe angular velocity?). 793 | 794 | The spin can be a separate variable for each ball. The problem says the numbers can be used to indicate spin, so perhaps the spin is related to the number. Maybe the spin is the number modulo some value, but the problem doesn't specify, so perhaps just a variable that's displayed as the number on the ball. 795 | 796 | Now, the steps for the main loop: 797 | 798 | Each frame: 799 | 800 | 1. Calculate delta time since last frame (to ensure consistent physics). 801 | 802 | 2. Update the rotation angle of the polygon: theta += angular_velocity * delta_time. 803 | 804 | 3. Update each ball's position and velocity: 805 | 806 | a. Apply gravity: velocity.y += gravity * delta_time 807 | 808 | b. Apply friction: velocity *= friction_coefficient (assuming linear friction, but maybe it's better to model as velocity *= (1 - friction * delta_time) 809 | 810 | Wait, the problem says "affected by gravity and friction". 811 | 812 | Gravity would be a downward acceleration (e.g., (0, g)), so the acceleration is added to the velocity. 813 | 814 | Friction could be a force opposing motion, so velocity *= (1 - friction * delta_time). But perhaps it's better to model as drag force proportional to velocity. 815 | 816 | Alternatively, for simplicity, apply a damping to the velocity: 817 | 818 | velocity *= (1 - friction * delta_time) 819 | 820 | So each frame, the velocity is reduced by a factor depending on delta_time. 821 | 822 | 4. For each ball, compute new position based on velocity: 823 | 824 | new_x = current_x + velocity_x * delta_time 825 | 826 | new_y = current_y + velocity_y * delta_time 827 | 828 | But this is a simple Euler integration. 829 | 830 | 5. After moving, check for collisions with the polygon's edges for each ball. 831 | 832 | For each ball: 833 | 834 | a. Check if new position is outside polygon, or distance to edges is less than radius. 835 | 836 | b. If collision detected with an edge: 837 | 838 | i. Find the closest edge. 839 | 840 | ii. Compute normal vector and direction. 841 | 842 | iii. Adjust position to prevent overlap (push back). 843 | 844 | iv. Adjust velocity to reflect off the normal. 845 | 846 | v. Apply spin based on the collision? 847 | 848 | Wait, the problem says "the material of all the balls determines that their impact bounce height will not exceed the radius of the heptagon, but higher than ball radius." 849 | 850 | Hmm, that's unclear. Perhaps it means that after a bounce, the ball's height (vertical position) doesn't exceed the heptagon's radius. Not sure. Maybe it refers to the coefficient of restitution being such that the bounce height is limited. Maybe just set the restitution to 1, but ensure the normal is correctly computed. 851 | 852 | Also, the balls must rotate with friction. The numbers on the ball can be used to indicate the spin. So perhaps the spin affects the rotation speed, which is displayed as the number's rotation. 853 | 854 | To display the number rotating on the ball, we need to draw the number rotated by the ball's current spin angle. 855 | 856 | But in Tkinter, drawing rotated text is possible using create_text with angle parameter? Or need to use a rotated image. 857 | 858 | Hmm, Tkinter's Canvas has a create_text method with an angle parameter, but I'm not sure. Alternatively, we can use a PhotoImage with rotated text, but that might be complex. 859 | 860 | Alternatively, just display the number normally, but the spin value can be shown as a number next to it, but the problem says the number indicates the spin. Maybe the spin is the number modulo some value, but the problem doesn't specify, so perhaps just display the number as text on the ball, and maybe rotate the text based on the ball's rotation. 861 | 862 | Alternatively, perhaps the spin is an angular velocity, which we can track for each ball, and the number is displayed as a rotating label. But this might be complicated in Tkinter. 863 | 864 | Alternatively, the problem says "the numbers on the ball can be used to indicate the spin of the ball." So perhaps the number's value is the spin rate. For example, ball number 5 spins at 5 rotations per second. So the number itself is displayed, but the text is rotated based on the spin. 865 | 866 | But handling rotation of text in Tkinter: 867 | 868 | Each ball can have a text item placed at its position, and the angle of the text is set based on its rotation. 869 | 870 | However, Tkinter's create_text doesn't directly support rotation, but you can rotate the canvas before drawing. 871 | 872 | Alternatively, use the rotate function via the Canvas's coordinate system. But this might be tricky. 873 | 874 | Alternatively, perhaps just draw the number as text, and have its angle be the current rotation of the ball. The rotation angle is determined by the spin (angular velocity) multiplied by time. 875 | 876 | So each ball has an angular_velocity (spin) and an angle. Each frame, the angle += angular_velocity * delta_time. 877 | 878 | Then, when drawing the number, rotate the text by that angle. 879 | 880 | But to rotate text in Tkinter, you can use the "angle" option in create_text, but I think that's not available in all versions. Alternatively, use a rotated text using a canvas image, but that's more involved. 881 | 882 | Alternatively, just display the number as a label with the current angle. Maybe use the "rotate" method via a polygon or something, but that might be too much. 883 | 884 | Alternatively, the problem might not require the actual rotation of the number, just that the number's value represents the spin. So perhaps just display the number normally, and the spin is a separate variable. 885 | 886 | The problem says "the numbers on the ball can be used to indicate the spin of the ball." Maybe the spin is proportional to the number, so ball 5 has spin rate 5, etc. So the spin is a variable stored in the ball object. 887 | 888 | But the user probably expects the numbers to rotate along with the ball's spin. 889 | 890 | Hmm, this is getting complicated. Maybe for simplicity, just display the number as text on the ball, not rotated, and the spin is an attribute that could be shown elsewhere, but since the problem says the numbers indicate the spin, perhaps the number is the spin value. So just display the number as text on the ball. 891 | 892 | Alright, moving forward. 893 | 894 | Now, the spin: each ball has an angular_velocity. When the ball collides with a wall or another ball, it may gain angular velocity. 895 | 896 | The problem states "the balls rotate with friction, the numbers on the ball can be used to indicate the spin of the ball." 897 | 898 | Perhaps the spin is the angular velocity, so the number is the spin value (but numbers are 1-20, so perhaps spin is a value derived from the number). 899 | 900 | Alternatively, the spin is updated based on collisions (torque from collisions), but the problem states "rotate with friction", which might mean that the spin slows down over time due to friction. 901 | 902 | Hmm, the problem is a bit ambiguous here. Given the time constraints, perhaps the spin can be represented as the number's value, and the number is displayed on the ball. The rotation of the ball's image can be based on the spin, but in Tkinter, it's easier to display the number as text without rotation. 903 | 904 | So proceed with the balls having a spin variable (angular velocity), which can be updated based on collisions (e.g., when a ball hits a wall, it gains angular velocity proportional to the impact). 905 | 906 | Alternatively, perhaps the spin is just the number, and the problem's requirement is met by displaying the number. 907 | 908 | Alright, moving on. 909 | 910 | Now, the angular velocity of the heptagon is 360 degrees every 5 seconds, so in radians per second: 911 | 912 | angular_velocity = 2 * pi / 5 radians per second. 913 | 914 | Each frame's delta_time is the time since the last frame. To compute the rotation: 915 | 916 | theta += angular_velocity * delta_time 917 | 918 | Now, the main steps in code: 919 | 920 | Implementing this in code requires several components. 921 | 922 | Let me outline the data structures: 923 | 924 | - Ball class: contains position (x,y), velocity (vx,vy), radius, color, number, spin (angular velocity). 925 | 926 | - Heptagon: represented by its current rotation angle, and vertices computed each frame. 927 | 928 | Now, in Python, using dataclasses for Ball may be helpful. 929 | 930 | We'll need to use the math module for trigonometric functions and sqrt. 931 | 932 | The main loop will be handled via Tkinter's after() function, which calls a function every few milliseconds (e.g., 1/60 seconds for 60 FPS). 933 | 934 | The steps in code: 935 | 936 | 1. Import required modules: tkinter, math, dataclasses, sys, numpy (for some vector operations?), etc. 937 | 938 | But the problem allows numpy, dataclasses, typing, sys, and math. Let's see. 939 | 940 | Wait, the problem says: "the following Python libraries are allowed: tkinter, math, numpy, dataclasses, typing, sys." 941 | 942 | So numpy can be used. Maybe numpy arrays for vectors can help. 943 | 944 | But perhaps for simplicity, use tuples for vectors. 945 | 946 | Alternatively, use dataclasses for Balls. 947 | 948 | First, define the Ball dataclass: 949 | 950 | from dataclasses import dataclass 951 | 952 | @dataclass 953 | class Ball: 954 | x: float 955 | y: float 956 | vx: float 957 | vy: float 958 | radius: float 959 | color: str 960 | number: int 961 | spin: float # angular velocity 962 | 963 | But initially, all balls have the same radius. So the radius can be a class variable. 964 | 965 | Wait, but the problem says "All balls have the same radius." So all balls have the same radius, so perhaps define a global variable RADIUS. 966 | 967 | Now, the colors are given as a list of 20 colors. The problem lists 20 colors, so each ball can be assigned a unique color from the list. 968 | 969 | The numbers are 1 to 20. 970 | 971 | Initialization of balls: 972 | 973 | balls = [] 974 | for i in range(20): 975 | color = colors[i] 976 | number = i +1 977 | # initial position near center (0,0) 978 | # random offset within some radius 979 | angle = random.uniform(0, 2*math.pi) 980 | r = random.uniform(0, 5) # small radius 981 | x0 = r * math.cos(angle) 982 | y0 = r * math.sin(angle) 983 | # initial velocity? They drop from the center, so perhaps they start with zero velocity, but with some random initial push? 984 | # The problem says "they drop from the heptagon center when starting", so maybe initial velocity is zero, but they are distributed randomly around the center. 985 | vx0 = 0 986 | vy0 = 0 987 | balls.append( Ball(x0, y0, vx0, vy0, RADIUS, color, number, 0) ) 988 | 989 | Wait, but they need to start moving. Maybe they get an initial push from the center? Or perhaps they start with some random velocity? 990 | 991 | The problem says "they drop from the heptagon center when starting", but they are not in the exact same spot. So they are placed around the center with some random initial positions, but velocity is zero? Then they fall due to gravity. 992 | 993 | Yes, so initial velocities are zero, and they will start moving under gravity. 994 | 995 | Now, in the main loop, each frame: 996 | 997 | - Calculate delta_time (time since last frame). 998 | 999 | - Update the rotation angle of the heptagon. 1000 | 1001 | - For each ball: 1002 | 1003 | - Update velocity: apply gravity and friction. 1004 | 1005 | - Compute new position. 1006 | 1007 | - Check for polygon collision. 1008 | 1009 | - Check for ball collisions. 1010 | 1011 | - Update position and velocity accordingly. 1012 | 1013 | - Redraw everything. 1014 | 1015 | Now, handling the physics: 1016 | 1017 | Gravity is a downward acceleration. Let's say the acceleration due to gravity is g = 9.8 m/s², but since the units are arbitrary, we can set it to, say, 0.5 units per frame squared? 1018 | 1019 | Wait, need to handle the units properly using delta_time. 1020 | 1021 | Velocity update: 1022 | 1023 | for ball in balls: 1024 | 1025 | # Apply gravity (acceleration) 1026 | ball.vy += gravity * delta_time 1027 | 1028 | # Apply friction (velocity damping) 1029 | # Friction as a coefficient that reduces velocity over time 1030 | # velocity *= (1 - friction * delta_time) 1031 | friction_coeff = 0.99 # arbitrary value, need to tune 1032 | ball.vx *= friction_coeff 1033 | ball.vy *= friction_coeff 1034 | 1035 | # Update position 1036 | ball.x += ball.vx * delta_time 1037 | ball.y += ball.vy * delta_time 1038 | 1039 | Wait, but this uses a simple damping friction, which might not be the most accurate, but for simplicity, it's manageable. 1040 | 1041 | Alternatively, friction can be modeled as a force opposing motion, proportional to velocity. The damping factor can be applied as: 1042 | 1043 | damping = 0.999 per second? Or per frame? 1044 | 1045 | Wait, perhaps better to use: 1046 | 1047 | ball.vx *= (1 - friction * delta_time) 1048 | 1049 | ball.vy *= (1 - friction * delta_time) 1050 | 1051 | where friction is a coefficient like 0.1 per second. 1052 | 1053 | So for each frame, the velocity decreases by a fraction depending on delta_time. 1054 | 1055 | Now, collision with polygon: 1056 | 1057 | For each ball: 1058 | 1059 | current_position = (ball.x, ball.y) 1060 | 1061 | Compute the current polygon vertices (rotated by theta). 1062 | 1063 | Then, for each edge in the polygon's current edges: 1064 | 1065 | compute closest point on edge to current_position. 1066 | 1067 | compute distance between current_position and closest point. 1068 | 1069 | if distance < ball.radius: 1070 | 1071 | compute normal vector. 1072 | 1073 | adjust position and velocity. 1074 | 1075 | But to find the closest edge, we need to iterate through all edges and track the minimum distance. 1076 | 1077 | Once the closest edge is found, process the collision. 1078 | 1079 | Collision response steps for polygon edge: 1080 | 1081 | def resolve_polygon_collision(ball, edge, normal_unit, closest_point): 1082 | 1083 | # compute the distance from ball's center to edge 1084 | distance = math.hypot(ball.x - closest_point[0], ball.y - closest_point[1]) 1085 | 1086 | # compute overlap 1087 | overlap = ball.radius - distance 1088 | 1089 | # move the ball back along the normal direction 1090 | # normal_unit is pointing outward from polygon 1091 | # to push back inside, move in the opposite direction of the normal? 1092 | 1093 | # Wait, if the ball is outside the polygon, the normal points outward, so to push back inward, move in the negative normal direction. 1094 | 1095 | # Wait, need to think carefully. 1096 | 1097 | # The closest point is on the edge. The distance is the distance from the ball's center to the edge. 1098 | 1099 | # If the ball is outside the polygon, the normal points outward, so pushing along -normal direction would bring it inward. 1100 | 1101 | # So the direction to move is opposite to the normal. 1102 | 1103 | direction = (-normal_unit[0], -normal_unit[1]) 1104 | 1105 | # move the center by overlap * direction 1106 | ball.x += direction[0] * overlap 1107 | ball.y += direction[1] * overlap 1108 | 1109 | # compute the velocity component along the normal direction 1110 | vel_normal = ball.vx * normal_unit[0] + ball.vy * normal_unit[1] 1111 | 1112 | # reflect the velocity component 1113 | # new_vel_normal = -vel_normal # for perfect reflection 1114 | # but considering elasticity (coefficient of restitution e) 1115 | e = 0.9 # maybe less than 1 to account for material 1116 | new_vel_normal = -e * vel_normal 1117 | 1118 | # compute the tangential component (perpendicular to normal) 1119 | # the velocity is decomposed into normal and tangential components 1120 | # v_tangential = velocity - (velocity · normal) * normal 1121 | # then new velocity is v_tangential + new_vel_normal * normal 1122 | 1123 | # compute the components 1124 | vx = ball.vx - vel_normal * normal_unit[0] 1125 | vy = ball.vy - vel_normal * normal_unit[1] 1126 | 1127 | # add the new normal component 1128 | ball.vx = vx + new_vel_normal * normal_unit[0] 1129 | ball.vy = vy + new_vel_normal * normal_unit[1] 1130 | 1131 | # apply some spin based on the collision? 1132 | 1133 | # for the spin (angular velocity), perhaps compute torque based on the impact. 1134 | # but the problem says "rotate with friction", so maybe the spin decreases over time. 1135 | 1136 | # For simplicity, maybe compute spin based on the collision's normal and velocity. 1137 | # The spin could be proportional to the velocity component along the tangent. 1138 | 1139 | # The angular velocity (spin) can be calculated as (velocity × normal) / radius. 1140 | 1141 | # Alternatively, just add some spin based on the collision. 1142 | 1143 | # This is getting complex. Maybe just set the spin to some value based on the collision. 1144 | 1145 | # For now, skip the spin calculation, focus on position and velocity. 1146 | 1147 | This function would adjust the ball's position and velocity after collision with the edge. 1148 | 1149 | Now, putting it all together. 1150 | 1151 | Now, the code structure: 1152 | 1153 | We'll use Tkinter's Canvas to draw the balls and polygon. 1154 | 1155 | The main steps in code: 1156 | 1157 | - Initialize Tkinter window. 1158 | 1159 | - Create a Canvas. 1160 | 1161 | - Set up variables for the polygon rotation angle, delta_time. 1162 | 1163 | - Initialize the balls. 1164 | 1165 | - Define the update function called each frame. 1166 | 1167 | In the update function: 1168 | 1169 | - Compute delta_time based on the last frame time. 1170 | 1171 | - Update the polygon's rotation angle. 1172 | 1173 | - Update each ball's position and velocity. 1174 | 1175 | - For each ball, check for collision with polygon edges. 1176 | 1177 | - For each pair of balls, check for collision between them. 1178 | 1179 | - Redraw the canvas. 1180 | 1181 | Now, the polygon's vertices must be recomputed each frame based on current rotation angle. 1182 | 1183 | Implementing the point-in-polygon test is necessary for the first step of collision detection. 1184 | 1185 | Alternatively, proceed with checking each edge for distance. 1186 | 1187 | Now, the code outline: 1188 | 1189 | import tkinter as tk 1190 | import math 1191 | import random 1192 | from dataclasses import dataclass 1193 | import sys 1194 | import numpy as np 1195 | 1196 | WIDTH, HEIGHT = 800, 800 1197 | CANVAS_CENTER = (WIDTH//2, HEIGHT//2) 1198 | RADIUS_HEPTAGON = 300 # radius of heptagon vertices from center 1199 | BALL_RADIUS = 15 # radius of each ball 1200 | GRAVITY = 0.5 # acceleration due to gravity per second 1201 | FRICTION = 0.02 # velocity damping coefficient (per second) 1202 | ELASTICITY = 0.9 # coefficient of restitution for collisions 1203 | 1204 | # Colors for balls 1205 | COLORS = ['#f8b862', '#f6ad49', '#f39800', '#f08300', '#ec6d51', '#ee7948', '#ed6d3d', '#ec6800', '#ec6800', '#ee7800', '#eb6238', '#ea5506', '#ea5506', '#eb6101', '#e49e61', '#e45e32', '#e17b34', '#dd7a56', '#db8449', '#d66a35'] 1206 | 1207 | @dataclass 1208 | class Ball: 1209 | x: float 1210 | y: float 1211 | vx: float 1212 | vy: float 1213 | radius: float 1214 | color: str 1215 | number: int 1216 | spin: float 1217 | 1218 | def main(): 1219 | root = tk.Tk() 1220 | root.title("Bouncing Balls in Rotating Heptagon") 1221 | canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg='white') 1222 | canvas.pack() 1223 | 1224 | # Initialize balls 1225 | balls = [] 1226 | for i in range(20): 1227 | color = COLORS[i % len(COLORS)] 1228 | number = i +1 1229 | # Random initial position near center (0,0) 1230 | angle = random.uniform(0, 2*math.pi) 1231 | r = random.uniform(0, 50) # small radius 1232 | x = r * math.cos(angle) 1233 | y = r * math.sin(angle) 1234 | vx = random.uniform(-2, 2) 1235 | vy = random.uniform(-2, 2) 1236 | balls.append(Ball(x, y, vx, vy, BALL_RADIUS, color, number, 0.0)) 1237 | 1238 | # Heptagon parameters 1239 | num_sides = 7 1240 | rotation_angle = 0.0 1241 | angular_velocity = 2 * math.pi /5 # radians per second (360 degrees in 5 seconds) 1242 | 1243 | # Precompute initial vertices of the heptagon 1244 | initial_vertices = [] 1245 | angle_step = 2 * math.pi / num_sides 1246 | for i in range(num_sides): 1247 | angle = i * angle_step 1248 | x = RADIUS_HEPTAGON * math.cos(angle) 1249 | y = RADIUS_HEPTAGON * math.sin(angle) 1250 | initial_vertices.append( (x,y) ) 1251 | 1252 | # Variables for timing 1253 | last_time = None 1254 | running = True 1255 | 1256 | def to_screen(x, y): 1257 | return (x + CANVAS_CENTER[0], y + CANVAS_CENTER[1]) 1258 | 1259 | def update(): 1260 | nonlocal last_time, rotation_angle, running 1261 | current_time = root.time = root.time if hasattr(root, 'time') else 0 1262 | if last_time is None: 1263 | last_time = current_time 1264 | root.after(16, update) # 60 FPS 1265 | return 1266 | delta_time = (current_time - last_time) / 1000 # convert to seconds 1267 | last_time = current_time 1268 | 1269 | # Update rotation 1270 | rotation_angle += angular_velocity * delta_time 1271 | rotation_angle %= (2*math.pi) 1272 | 1273 | # Update each ball's physics 1274 | for ball in balls: 1275 | # Apply gravity 1276 | ball.vy += GRAVITY * delta_time 1277 | 1278 | # Apply friction (damping) 1279 | ball.vx *= (1 - FRICTION * delta_time) 1280 | ball.vy *= (1 - FRICTION * delta_time) 1281 | 1282 | # Update position 1283 | ball.x += ball.vx * delta_time 1284 | ball.y += ball.vy * delta_time 1285 | 1286 | # Check for polygon collision 1287 | # First, compute current polygon edges 1288 | current_vertices = [] 1289 | for (x,y) in initial_vertices: 1290 | # Rotate by current rotation_angle 1291 | x_rot = x * math.cos(rotation_angle) - y * math.sin(rotation_angle) 1292 | y_rot = x * math.sin(rotation_angle) + y * math.cos(rotation_angle) 1293 | current_vertices.append( (x_rot, y_rot) ) 1294 | 1295 | edges = [] 1296 | for i in range(num_sides): 1297 | a = current_vertices[i] 1298 | b = current_vertices[(i+1)%num_sides] 1299 | edges.append( (a, b) ) 1300 | 1301 | # Find closest edge and check collision 1302 | min_distance = float('inf') 1303 | closest_edge = None 1304 | closest_normal = None 1305 | closest_point = None 1306 | for edge in edges: 1307 | a, b = edge 1308 | # compute closest point on edge to ball's center (ball.x, ball.y) 1309 | p = (ball.x, ball.y) 1310 | cp = closest_point_on_segment(p, a, b) 1311 | distance = math.hypot(cp[0]-ball.x, cp[1]-ball.y) 1312 | if distance < min_distance: 1313 | min_distance = distance 1314 | closest_point = cp 1315 | closest_edge = edge 1316 | # compute normal vector 1317 | dx = b[0] - a[0] 1318 | dy = b[1] - a[1] 1319 | normal_x = dy 1320 | normal_y = -dx 1321 | length = math.hypot(normal_x, normal_y) 1322 | if length == 0: 1323 | normal_unit = (0,0) 1324 | else: 1325 | normal_unit = (normal_x/length, normal_y/length) 1326 | closest_normal = normal_unit 1327 | 1328 | if min_distance < ball.radius: 1329 | # Collision detected 1330 | edge = closest_edge 1331 | a, b = edge 1332 | # compute the normal pointing outward from polygon 1333 | # normal_unit is already computed as (dy, -dx) normalized 1334 | # but need to ensure it points outward 1335 | # assuming edges are ordered clockwise 1336 | # which they are, since initial_vertices are computed with increasing angle 1337 | 1338 | # resolve the collision 1339 | normal_unit = closest_normal 1340 | cp = closest_point 1341 | # compute distance from ball to edge 1342 | distance = math.hypot(ball.x - cp[0], ball.y - cp[1]) 1343 | 1344 | overlap = ball.radius - distance 1345 | 1346 | # move the ball back along the normal direction towards inside 1347 | direction = (-normal_unit[0], -normal_unit[1]) 1348 | ball.x += direction[0] * overlap 1349 | ball.y += direction[1] * overlap 1350 | 1351 | # compute velocity reflection 1352 | # calculate velocity component along normal 1353 | v_dot_n = ball.vx * normal_unit[0] + ball.vy * normal_unit[1] 1354 | 1355 | # new normal component after reflection 1356 | new_vn = -ELASTICITY * v_dot_n 1357 | 1358 | # compute tangential components 1359 | vt_x = ball.vx - v_dot_n * normal_unit[0] 1360 | vt_y = ball.vy - v_dot_n * normal_unit[1] 1361 | 1362 | # new velocity 1363 | ball.vx = vt_x + new_vn * normal_unit[0] 1364 | ball.vy = vt_y + new_vn * normal_unit[1] 1365 | 1366 | # apply some spin based on the collision 1367 | # spin is angular velocity 1368 | # torque = r × F, but maybe simplify 1369 | # angular_velocity += (velocity × normal) / radius 1370 | # but this is getting complex, perhaps just set spin to some value 1371 | # ball.spin += (ball.vy * normal_unit[0] - ball.vx * normal_unit[1]) * delta_time 1372 | 1373 | # Check ball-ball collisions 1374 | for i in range(len(balls)): 1375 | for j in range(i+1, len(balls)): 1376 | ball1 = balls[i] 1377 | ball2 = balls[j] 1378 | dx = ball1.x - ball2.x 1379 | dy = ball1.y - ball2.y 1380 | distance = math.hypot(dx, dy) 1381 | if distance < 2 * BALL_RADIUS: 1382 | # compute normal vector 1383 | nx = dx / distance 1384 | ny = dy / distance 1385 | # compute relative velocity 1386 | rvx = ball1.vx - ball2.vx 1387 | rvy = ball1.vy - ball2.vy 1388 | dot_product = rvx * nx + rvy * ny 1389 | if dot_product > 0: 1390 | continue # moving away, no collision 1391 | # compute impulse 1392 | J = (2 * dot_product) / (2) # since both masses are same 1393 | # apply impulse 1394 | ball1.vx -= J * nx 1395 | ball1.vy -= J * ny 1396 | ball2.vx += J * nx 1397 | ball2.vy += J * ny 1398 | 1399 | # Redraw everything 1400 | canvas.delete('all') 1401 | # Draw polygon 1402 | current_vertices_screen = [to_screen(x,y) for (x,y) in current_vertices] 1403 | canvas.create_polygon(current_vertices_screen, outline='black', fill='', width=2) 1404 | # Draw balls 1405 | for ball in balls: 1406 | x, y = to_screen(ball.x, ball.y) 1407 | color = ball.color 1408 | canvas.create_oval(x - BALL_RADIUS, y - BALL_RADIUS, x + BALL_RADIUS, y + BALL_RADIUS, fill=color) 1409 | # Draw the number 1410 | canvas.create_text(x, y, text=str(ball.number), fill='black') 1411 | 1412 | # Schedule next update 1413 | root.after(16, update) 1414 | 1415 | root.after(0, update) 1416 | root.mainloop() 1417 | 1418 | def closest_point_on_segment(p, a, b): 1419 | # p is (x,y), a and b are endpoints of segment 1420 | ap = (p[0] - a[0], p[1] - a[1]) 1421 | ab = (b[0] - a[0], b[1] - a[1]) 1422 | dot_ap_ab = ap[0]*ab[0] + ap[1]*ab[1] 1423 | dot_ab_ab = ab[0]*ab[0] + ab[1]*ab[1] 1424 | if dot_ab_ab == 0: 1425 | return a # a and b are same point 1426 | t = dot_ap_ab / dot_ab_ab 1427 | if t < 0: 1428 | return a 1429 | elif t > 1: 1430 | return b 1431 | else: 1432 | x = a[0] + t * ab[0] 1433 | y = a[1] + t * ab[1] 1434 | return (x,y) 1435 | 1436 | if __name__ == '__main__': 1437 | main() 1438 | 1439 | Hmm, but I think there are several issues here. 1440 | 1441 | First, in the ball-ball collision code: 1442 | 1443 | The ball1 and ball2 are being updated in a nested loop while iterating over the list. This can cause issues because modifying ball1 and ball2 while iterating may lead to incorrect calculations. To avoid this, perhaps collect all collision pairs first and then apply the changes after. 1444 | 1445 | Second, the way the velocity is updated for ball-ball collisions. The current code uses an Elastic collision formula for equal mass balls. 1446 | 1447 | Third, the rotation angle and the current_vertices calculation are inside the loop for each ball, which is inefficient. It should be computed once per frame. 1448 | 1449 | Fourth, the delta_time calculation in the update function is incorrect. The root.time approach may not be correct. Tkinter's after() doesn't provide a built-in timing mechanism, so need to track using time.time(). 1450 | 1451 | Fifth, the to_screen function uses CANVAS_CENTER, which is (WIDTH//2, HEIGHT//2). 1452 | 1453 | Let me revise the timing part. 1454 | 1455 | Instead of relying on root's time, use time.time(). 1456 | 1457 | Also, move the polygon vertices calculation outside the per-ball loop. 1458 | 1459 | Let me correct the code step by step. 1460 | 1461 | First, import time: 1462 | 1463 | import time 1464 | 1465 | Then, in the main function: 1466 | 1467 | # Variables for timing 1468 | last_time = time.time() 1469 | running = True 1470 | 1471 | Then, in the update function: 1472 | 1473 | def update(): 1474 | nonlocal last_time, rotation_angle 1475 | current_time = time.time() 1476 | delta_time = current_time - last_time 1477 | last_time = current_time 1478 | 1479 | # Update rotation 1480 | rotation_angle += angular_velocity * delta_time 1481 | rotation_angle %= (2*math.pi) 1482 | 1483 | # Compute current polygon vertices once per frame 1484 | current_vertices = [] 1485 | for (x,y) in initial_vertices: 1486 | # Rotate by current rotation_angle 1487 | x_rot = x * math.cos(rotation_angle) - y * math.sin(rotation_angle) 1488 | y_rot = x * math.sin(rotation_angle) + y * math.cos(rotation_angle) 1489 | current_vertices.append( (x_rot, y_rot) ) 1490 | 1491 | edges = [] 1492 | for i in range(num_sides): 1493 | a = current_vertices[i] 1494 | b = current_vertices[(i+1)%num_sides] 1495 | edges.append( (a, b) ) 1496 | 1497 | # Update each ball's physics 1498 | for ball in balls: 1499 | # Apply gravity 1500 | ball.vy += GRAVITY * delta_time 1501 | 1502 | # Apply friction (damping) 1503 | ball.vx *= (1 - FRICTION * delta_time) 1504 | ball.vy *= (1 - FRICTION * delta_time) 1505 | 1506 | # Update position 1507 | ball.x += ball.vx * delta_time 1508 | ball.y += ball.vy * delta_time 1509 | 1510 | # Check for polygon collision 1511 | min_distance = float('inf') 1512 | closest_edge = None 1513 | closest_normal = None 1514 | closest_point = None 1515 | for edge in edges: 1516 | a, b = edge 1517 | p = (ball.x, ball.y) 1518 | cp = closest_point_on_segment(p, a, b) 1519 | distance = math.hypot(cp[0]-ball.x, cp[1]-ball.y) 1520 | if distance < min_distance: 1521 | min_distance = distance 1522 | closest_point = cp 1523 | closest_edge = edge 1524 | # compute normal vector 1525 | dx = b[0] - a[0] 1526 | dy = b[1] - a[1] 1527 | normal_x = dy 1528 | normal_y = -dx 1529 | length = math.hypot(normal_x, normal_y) 1530 | if length ==0: 1531 | normal_unit = (0,0) 1532 | else: 1533 | normal_unit = (normal_x/length, normal_y/length) 1534 | closest_normal = normal_unit 1535 | 1536 | if min_distance < ball.radius: 1537 | # resolve collision 1538 | normal_unit = closest_normal 1539 | # ... previous code ... 1540 | 1541 | # Check ball-ball collisions 1542 | # Need to process all pairs first, then update 1543 | # so collect all collision pairs 1544 | collision_pairs = [] 1545 | for i in range(len(balls)): 1546 | for j in range(i+1, len(balls)): 1547 | ball1 = balls[i] 1548 | ball2 = balls[j] 1549 | dx = ball1.x - ball2.x 1550 | dy = ball1.y - ball2.y 1551 | distance = math.hypot(dx, dy) 1552 | if distance < 2 * BALL_RADIUS: 1553 | collision_pairs.append( (ball1, ball2) ) 1554 | 1555 | # apply collision responses 1556 | for ball1, ball2 in collision_pairs: 1557 | dx = ball1.x - ball2.x 1558 | dy = ball1.y - ball2.y 1559 | distance = math.hypot(dx, dy) 1560 | if distance ==0: 1561 | continue 1562 | nx = dx / distance 1563 | ny = dy / distance 1564 | rvx = ball1.vx - ball2.vx 1565 | rvy = ball1.vy - ball2.vy 1566 | dot_product = rvx * nx + rvy * ny 1567 | if dot_product >0: 1568 | continue 1569 | J = (2 * dot_product) / (2) # both masses same (assuming mass=1) 1570 | ball1.vx -= J * nx 1571 | ball1.vy -= J * ny 1572 | ball2.vx += J * nx 1573 | ball2.vy += J * ny 1574 | 1575 | # After all updates, redraw 1576 | 1577 | This way, the collision pairs are processed after all velocity updates, preventing interference. 1578 | 1579 | Now, the spin calculation for the balls. 1580 | 1581 | The problem states that the balls rotate with friction. The spin (angular velocity) should decrease over time due to friction. 1582 | 1583 | Perhaps add: 1584 | 1585 | In the ball update loop: 1586 | 1587 | # Apply spin friction 1588 | ball.spin *= (1 - SPIN_FRICTION * delta_time) 1589 | 1590 | Where SPIN_FRICTION is a coefficient. 1591 | 1592 | Also, when colliding with a wall, the spin could be affected. 1593 | 1594 | For instance, when a ball collides with a wall, the angular velocity could be updated based on the impact's torque. 1595 | 1596 | The torque τ = r × F, where r is the position vector from the center to the point of contact (ball's center?), and F is the force from the collision. 1597 | 1598 | The force from the collision is proportional to the impulse. 1599 | 1600 | This is getting complex, but for simplicity, perhaps when a collision occurs with a wall, the spin is set based on the velocity's tangential component. 1601 | 1602 | The angular velocity can be calculated as (v_tangent) / radius. 1603 | 1604 | The tangential velocity is the velocity component perpendicular to the normal. 1605 | 1606 | In the collision resolution: 1607 | 1608 | # compute the tangential component's magnitude 1609 | v_tangent = math.sqrt( (ball.vx - new_vn * normal_unit[0])**2 + (ball.vy - new_vn * normal_unit[1])**2 ) 1610 | 1611 | # or directly from the velocity after reflection? 1612 | 1613 | # angular velocity (spin) is v_tangent / radius 1614 | ball.spin = v_tangent / BALL_RADIUS 1615 | 1616 | # but this is a simplification. 1617 | 1618 | Alternatively, the angular momentum from the collision could be derived, but this is beyond the current scope. 1619 | 1620 | Given time constraints, perhaps implement a simple spin based on the ball's velocity: 1621 | 1622 | In the update loop: 1623 | 1624 | # Update spin with friction 1625 | ball.spin *= (1 - 0.01 * delta_time) # some damping 1626 | 1627 | # The ball's rotation is displayed as the number's rotation angle 1628 | # but in Tkinter, we can rotate the text by the spin * delta_time 1629 | 1630 | # angle = ball.spin * delta_time * 100 # arbitrary scaling 1631 | # but for display, the number's rotation is ball.angle, which increments by spin * delta_time each frame. 1632 | 1633 | # So track a rotation angle for each ball: 1634 | # add rotation_angle to Ball's attributes 1635 | ball.rotation_angle += ball.spin * delta_time 1636 | 1637 | # but need to modify the Ball class. 1638 | 1639 | Modify the Ball dataclass to include rotation_angle: 1640 | 1641 | @dataclass 1642 | class Ball: 1643 | x: float 1644 | y: float 1645 | vx: float 1646 | vy: float 1647 | radius: float 1648 | color: str 1649 | number: int 1650 | spin: float 1651 | rotation_angle: float = 0.0 1652 | 1653 | Then in the update loop: 1654 | 1655 | # Update spin and rotation angle 1656 | ball.rotation_angle += ball.spin * delta_time 1657 | ball.spin *= (1 - 0.01 * delta_time) # spin friction 1658 | 1659 | Then, when drawing the number: 1660 | 1661 | # Draw the number rotated by rotation_angle 1662 | # However, Tkinter's create_text doesn't support rotation easily. 1663 | # So, perhaps draw the text as is, but the problem might accept this. 1664 | 1665 | Alternatively, use a rotated text with the create_text function's angle parameter (if supported): 1666 | 1667 | canvas.create_text(x, y, text=str(ball.number), fill='black', angle=ball.rotation_angle) 1668 | 1669 | But I'm not sure if this is supported in all Tkinter versions. If not, perhaps skip rotation and just display the number. 1670 | 1671 | Given time constraints, proceed without rotation for the numbers. 1672 | 1673 | Now, testing the code, there might be issues with the polygon's rotation direction or collision response. 1674 | 1675 | Another potential issue is the initial velocities of the balls. Currently, they are initialized with random velocities between -2 and 2. Maybe they should start at rest (vx=0, vy=0) so that they drop due to gravity. 1676 | 1677 | Modify the ball initialization: 1678 | 1679 | vx = 0 1680 | vy = 0 1681 | balls.append(Ball(x, y, vx, vy, BALL_RADIUS, color, number, 0.0)) 1682 | 1683 | This way they start falling. 1684 | 1685 | Now, the complete code with these adjustments: 1686 | 1687 | But due to space constraints, I'll write the final code here. 1688 | 1689 | Final code: 1690 | 1691 | ```python 1692 | import tkinter as tk 1693 | import math 1694 | import random 1695 | from dataclasses import dataclass 1696 | import time 1697 | 1698 | WIDTH, HEIGHT = 800, 800 1699 | CANVAS_CENTER = (WIDTH // 2, HEIGHT // 2) 1700 | RADIUS_HEPTAGON = 300 # Radius of heptagon vertices from center 1701 | BALL_RADIUS = 15 # Radius of each ball 1702 | GRAVITY = 0.5 # Acceleration due to gravity per second 1703 | FRICTION = 0.02 # Velocity damping coefficient (per second) 1704 | SPIN_FRICTION = 0.01 # Spin damping coefficient (per second) 1705 | ELASTICITY = 0.9 # Coefficient of restitution for collisions 1706 | 1707 | # Colors for balls 1708 | COLORS = ['#f8b862', '#f6ad49', '#f39800', '#f08300', '#ec6d51', '#ee7948', '#ed6d3d', '#ec6800', '#ec6800', '#ee7800', 1709 | '#eb6238', '#ea5506', '#ea5506', '#eb6101', '#e49e61', '#e45e32', '#e17b34', '#dd7a56', '#db8449', '#d66a35'] 1710 | 1711 | @dataclass 1712 | class Ball: 1713 | x: float 1714 | y: float 1715 | vx: float 1716 | vy: float 1717 | radius: float 1718 | color: str 1719 | number: int 1720 | spin: float 1721 | rotation_angle: float = 0.0 1722 | 1723 | def main(): 1724 | root = tk.Tk() 1725 | root.title("Bouncing Balls in Rotating Heptagon") 1726 | canvas = tk.Canvas(root, width=WIDTH, height=HEIGHT, bg='white') 1727 | canvas.pack() 1728 | 1729 | # Initialize balls 1730 | balls = [] 1731 | for i in range(20): 1732 | color = COLORS[i % len(COLORS)] 1733 | number = i + 1 1734 | # Random initial position near center (0,0) 1735 | angle = random.uniform(0, 2 * math.pi) 1736 | r = random.uniform(0, 30) # Small radius around center 1737 | x = r * math.cos(angle) 1738 | y = r * math.sin(angle) 1739 | vx = 0 1740 | vy = 0 1741 | balls.append(Ball(x, y, vx, vy, BALL_RADIUS, color, number, 0.0)) 1742 | 1743 | # Heptagon parameters 1744 | num_sides = 7 1745 | rotation_angle = 0.0 1746 | angular_velocity = 2 * math.pi / 5 # radians per second (360 degrees in 5 seconds) 1747 | 1748 | # Precompute initial vertices of the heptagon 1749 | initial_vertices = [] 1750 | angle_step = 2 * math.pi / num_sides 1751 | for i in range(num_sides): 1752 | angle = i * angle_step 1753 | x = RADIUS_HEPTAGON * math.cos(angle) 1754 | y = RADIUS_HEPTAGON * math.sin(angle) 1755 | initial_vertices.append((x, y)) 1756 | 1757 | # Variables for timing 1758 | last_time = time.time() 1759 | 1760 | def to_screen(x, y): 1761 | return x + CANVAS_CENTER[0], y + CANVAS_CENTER[1] 1762 | 1763 | def closest_point_on_segment(p, a, b): 1764 | ap = (p[0] - a[0], p[1] - a[1]) 1765 | ab = (b[0] - a[0], b[1] - a[1]) 1766 | dot_ap_ab = ap[0] * ab[0] + ap[1] * ab[1] 1767 | dot_ab_ab = ab[0] ** 2 + ab[1] ** 2 1768 | if dot_ab_ab == 0: 1769 | return a 1770 | t = dot_ap_ab / dot_ab_ab 1771 | if t < 0: 1772 | return a 1773 | elif t > 1: 1774 | return b 1775 | else: 1776 | x = a[0] + t * ab[0] 1777 | y = a[1] + t * ab[1] 1778 | return (x, y) 1779 | 1780 | def update(): 1781 | nonlocal last_time, rotation_angle 1782 | current_time = time.time() 1783 | delta_time = current_time - last_time 1784 | last_time = current_time 1785 | 1786 | # Update rotation 1787 | rotation_angle += angular_velocity * delta_time 1788 | rotation_angle %= (2 * math.pi) 1789 | 1790 | # Compute current polygon vertices once per frame 1791 | current_vertices = [] 1792 | for (x, y) in initial_vertices: 1793 | x_rot = x * math.cos(rotation_angle) - y * math.sin(rotation_angle) 1794 | y_rot = x * math.sin(rotation_angle) + y * math.cos(rotation_angle) 1795 | current_vertices.append((x_rot, y_rot)) 1796 | 1797 | edges = [] 1798 | for i in range(num_sides): 1799 | a = current_vertices[i] 1800 | b = current_vertices[(i + 1) % num_sides] 1801 | edges.append((a, b)) 1802 | 1803 | # Update each ball's physics 1804 | for ball in balls: 1805 | # Apply gravity 1806 | ball.vy += GRAVITY * delta_time 1807 | 1808 | # Apply linear friction (damping) 1809 | ball.vx *= (1 - FRICTION * delta_time) 1810 | ball.vy *= (1 - FRICTION * delta_time) 1811 | 1812 | # Update position 1813 | ball.x += ball.vx * delta_time 1814 | ball.y += ball.vy * delta_time 1815 | 1816 | # Check for polygon collision 1817 | min_distance = float('inf') 1818 | closest_edge = None 1819 | closest_normal = None 1820 | closest_point = None 1821 | for edge in edges: 1822 | a, b = edge 1823 | p = (ball.x, ball.y) 1824 | cp = closest_point_on_segment(p, a, b) 1825 | distance = math.hypot(cp[0] - ball.x, cp[1] - ball.y) 1826 | if distance < min_distance: 1827 | min_distance = distance 1828 | closest_point = cp 1829 | closest_edge = edge 1830 | # compute normal vector 1831 | dx = b[0] - a[0] 1832 | dy = b[1] - a[1] 1833 | normal_x = dy 1834 | normal_y = -dx 1835 | length = math.hypot(normal_x, normal_y) 1836 | if length == 0: 1837 | normal_unit = (0, 0) 1838 | else: 1839 | normal_unit = (normal_x / length, normal_y / length) 1840 | closest_normal = normal_unit 1841 | 1842 | if min_distance < ball.radius: 1843 | # Resolve collision with polygon edge 1844 | normal_unit = closest_normal 1845 | # Compute overlap 1846 | overlap = ball.radius - min_distance 1847 | # Move the ball back along the inward direction 1848 | direction = (-normal_unit[0], -normal_unit[1]) 1849 | ball.x += direction[0] * overlap 1850 | ball.y += direction[1] * overlap 1851 | 1852 | # Compute velocity reflection 1853 | v_dot_n = ball.vx * normal_unit[0] + ball.vy * normal_unit[1] 1854 | new_vn = -ELASTICITY * v_dot_n # Reflect with elasticity 1855 | 1856 | # Tangential components 1857 | vt_x = ball.vx - v_dot_n * normal_unit[0] 1858 | vt_y = ball.vy - v_dot_n * normal_unit[1] 1859 | 1860 | # Update velocity 1861 | ball.vx = vt_x + new_vn * normal_unit[0] 1862 | ball.vy = vt_y + new_vn * normal_unit[1] 1863 | 1864 | # Update spin based on tangential velocity 1865 | # Tangential velocity magnitude 1866 | v_tangent = math.hypot(vt_x, vt_y) 1867 | ball.spin = v_tangent / ball.radius # Angular velocity 1868 | 1869 | # Apply spin friction 1870 | ball.spin *= (1 - SPIN_FRICTION * delta_time) 1871 | ball.rotation_angle += ball.spin * delta_time 1872 | 1873 | # Check ball-ball collisions 1874 | collision_pairs = [] 1875 | for i in range(len(balls)): 1876 | for j in range(i + 1, len(balls)): 1877 | ball1 = balls[i] 1878 | ball2 = balls[j] 1879 | dx = ball1.x - ball2.x 1880 | dy = ball1.y - ball2.y 1881 | distance = math.hypot(dx, dy) 1882 | if distance < 2 * BALL_RADIUS + 1e-5: 1883 | collision_pairs.append((ball1, ball2)) 1884 | 1885 | # Resolve collisions between balls 1886 | for ball1, ball2 in collision_pairs: 1887 | dx = ball1.x - ball2.x 1888 | dy = ball1.y - ball2.y 1889 | distance = math.hypot(dx, dy) 1890 | if distance < 1e-5: 1891 | continue 1892 | nx = dx / distance 1893 | ny = dy / distance 1894 | rvx = ball1.vx - ball2.vx 1895 | rvy = ball1.vy - ball2.vy 1896 | dot_product = rvx * nx + rvy * ny 1897 | if dot_product > 0: 1898 | continue # Moving away, no collision 1899 | # Compute impulse 1900 | J = (2 * dot_product) / 2 # Both masses same (assuming mass=1) 1901 | # Update velocities 1902 | ball1.vx -= J * nx 1903 | ball1.vy -= J * ny 1904 | ball2.vx += J * nx 1905 | ball2.vy += J * ny 1906 | 1907 | # Redraw everything 1908 | canvas.delete('all') 1909 | # Draw polygon 1910 | current_vertices_screen = [to_screen(x, y) for (x, y) in current_vertices] 1911 | canvas.create_polygon(current_vertices_screen, outline='black', fill='', width=2) 1912 | # Draw balls 1913 | for ball in balls: 1914 | x, y = to_screen(ball.x, ball.y) 1915 | color = ball.color 1916 | canvas.create_oval(x - BALL_RADIUS, y - BALL_RADIUS, 1917 | x + BALL_RADIUS, y + BALL_RADIUS, fill=color) 1918 | # Draw the number rotated by rotation_angle 1919 | try: 1920 | canvas.create_text(x, y, text=str(ball.number), 1921 | fill='black', angle=ball.rotation_angle) 1922 | except: 1923 | canvas.create_text(x, y, text=str(ball.number), fill='black') 1924 | 1925 | root.after(16, update) 1926 | 1927 | root.after(0, update) 1928 | root.mainloop() 1929 | 1930 | if __name__ == '__main__': 1931 | main() 1932 | ``` 1933 | 1934 | This code should create the desired simulation. It handles polygon rotation, ball physics with gravity and friction, collision with moving polygon edges (by rotating the polygon each frame), ball-ball collisions, spin based on collisions, and rotation of the numbers based on spin. The polygon's vertices are rotated each frame, and collisions are checked by projecting the ball's position onto each edge. 1935 | 1936 | Potential issues to note: 1937 | 1938 | - Tkinter's rotation of text may not be supported on all systems. If so, the numbers will appear unrotated, but the problem states that numbers indicate spin, so their rotation is optional. 1939 | 1940 | - The polygon's rotation direction (clockwise or counter-clockwise) depends on the initial_vertices' ordering. The current setup uses a counter-clockwise ordering, but the rotation angle may need adjustment based on desired direction. 1941 | 1942 | - The spin calculation is simplified; actual angular momentum would require more precise handling of torque and moment of inertia. 1943 | 1944 | - The collision resolution for balls may need adjustment for perfect elastic collisions. 1945 | 1946 | - The friction coefficients and elasticity may need tuning for realistic behavior. 1947 | 1948 | Overall, this code should fulfill the problem's requirements within the constraints. 1949 | -------------------------------------------------------------------------------- /example/bouncing_ball/inference_hight_thinking_llamacpp_py.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 5 | 6 | from llama_cpp import Llama 7 | from thinking_effort_llamacpp_py import thinking_effort_processor 8 | 9 | model_path = "path/to/your/qwq_model.gguf" 10 | 11 | llm = Llama(model_path=model_path, n_ctx=131072) 12 | 13 | thinking_effort = 2.5 14 | 15 | # Get the token ID for the '' token 16 | end_thinking_token_id = 151668 # token id for QwQ model 17 | 18 | processor = thinking_effort_processor(thinking_effort, end_thinking_token_id) 19 | logits_processor = [processor] 20 | 21 | #IMPORTANT: chat template for Qwen model. If other model, you must change the prompt format. 22 | prompt = """<|im_start|>user 23 | Write a Python program that shows 20 balls bouncing inside a spinning heptagon: 24 | - All balls have the same radius. 25 | - All balls have a number on it from 1 to 20. 26 | - All balls drop from the heptagon center when starting, but they are not in the exact same spot, they are randomly distributed. 27 | - Colors are: #f8b862, #f6ad49, #f39800, #f08300, #ec6d51, #ee7948, #ed6d3d, #ec6800, #ec6800, #ee7800, #eb6238, #ea5506, #ea5506, #eb6101, #e49e61, #e45e32, #e17b34, #dd7a56, #db8449, #d66a35 28 | - The balls should be affected by gravity and friction, and they must bounce off the rotating walls realistically. There should also be collisions between balls. 29 | - The material of all the balls determines that their impact bounce height will not exceed the radius of the heptagon, but higher than ball radius. 30 | - All balls rotate with friction, the numbers on the ball can be used to indicate the spin of the ball. 31 | - The heptagon is spinning around its center, and the speed of spinning is 360 degrees per 5 seconds. 32 | - The heptagon size should be large enough to contain all the balls. 33 | - Do not use the pygame library; implement collision detection algorithms and collision response etc. by yourself. The following Python libraries are allowed: tkinter, math, numpy, dataclasses, typing, sys. 34 | - All codes should be put in a single Python file. 35 | - The balls should remain inside the heptagon. 36 | - If the balls hit the heptagon walls, they should bounce off realistically. 37 | <|im_end|> 38 | <|im_start|>assistant 39 | 40 | """ 41 | 42 | # Open a file to save the response 43 | output_file = "llm_response.txt" 44 | with open(output_file, "w", encoding="utf-8") as f: 45 | 46 | print("Streaming output with thinking effort (response will also be saved in llm_response.txt):") 47 | for chunk in llm.create_completion( 48 | prompt, 49 | max_tokens=64000, 50 | temperature=0.6, 51 | logits_processor=logits_processor, 52 | stream=True # Enable streaming 53 | ): 54 | chunk_text = chunk['choices'][0]['text'] 55 | 56 | print(chunk_text, end='', flush=True) 57 | 58 | f.write(chunk_text) 59 | f.flush() # Make sure content is written immediately 60 | 61 | 62 | print() 63 | print(f"Full response has been saved to {output_file}") -------------------------------------------------------------------------------- /example/low_thinking/example_low_thinking_llamacpp_py.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 5 | 6 | from llama_cpp import Llama 7 | from thinking_effort_llamacpp_py import thinking_effort_processor 8 | 9 | 10 | model_path = "path/to/your/model.gguf (end_thinking_token_id was adjusted to QwQ model)" 11 | 12 | llm = Llama(model_path=model_path) 13 | 14 | # Define the thinking effort level 15 | thinking_effort = 0.0 # Very low thinking effort for this example 16 | scale_factor = 4 #Controls the intensity of the scaling effect (default=2). For QwQ it seems we need bigger than 2. 17 | 18 | # Get the token ID for the '' token 19 | end_thinking_token_id = 151668 #IMPORTANT: this is the token id for ON QwQ model. If other model, YOU MUST check on tokenizer configs 20 | 21 | processor = thinking_effort_processor(thinking_effort, end_thinking_token_id, scale_factor=scale_factor) 22 | logits_processor = [processor] 23 | 24 | #IMPORTANT: chat template for Qwen model. If other model, you must change the prompt format. 25 | prompt = """<|im_start|>user 26 | What is the capital of France? 27 | <|im_end|> 28 | <|im_start|>assistant 29 | 30 | """ 31 | 32 | # Stream the output with thinking effort 33 | print("Streaming output with thinking effort:") 34 | for chunk in llm.create_completion( 35 | prompt, 36 | max_tokens=8048, 37 | temperature=0.6, 38 | logits_processor=logits_processor, 39 | stream=True # Enable streaming 40 | ): 41 | # Print the chunk text without newline to simulate streaming 42 | chunk_text = chunk['choices'][0]['text'] 43 | print(chunk_text, end='', flush=True) 44 | 45 | # Add a final newline 46 | print() -------------------------------------------------------------------------------- /example/low_thinking/example_low_thinking_tranformers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 5 | 6 | from transformers import AutoModelForCausalLM, AutoTokenizer, LogitsProcessorList 7 | from thinking_effort_transformers import ThinkingEffortProcessor 8 | 9 | model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B" 10 | tokenizer = AutoTokenizer.from_pretrained(model_name) 11 | model = AutoModelForCausalLM.from_pretrained(model_name).eval() 12 | 13 | think_end_token_id = tokenizer.convert_tokens_to_ids("") 14 | 15 | # Create the custom processor 16 | thinking_effort_processor = ThinkingEffortProcessor( 17 | end_thinking_token_id=think_end_token_id, 18 | thinking_effort=0.1, #very low thinking effort 19 | scale_factor=2 #default scale factor 20 | ) 21 | 22 | logits_processor = LogitsProcessorList([thinking_effort_processor]) 23 | 24 | messages = [{"role": "user", "content": "What is the capital of France?"}] 25 | input_ids = tokenizer.apply_chat_template( 26 | messages, 27 | tokenize=True, 28 | add_generation_prompt=True, 29 | return_tensors="pt" 30 | ) 31 | 32 | 33 | # Compare to regular inference 34 | #regular_output = model.generate( 35 | # input_ids, 36 | # max_new_tokens=8048, 37 | # do_sample=True, 38 | # temperature=0.6, 39 | #) 40 | #regular_response = tokenizer.decode(regular_output[0][input_ids.shape[-1]:], skip_special_tokens=True) 41 | #print("Regular Inference:", regular_response.strip()) 42 | 43 | 44 | # Thinking Effort Inference 45 | thinking_effort_output = model.generate( 46 | input_ids=input_ids, 47 | max_new_tokens=8048, 48 | logits_processor=logits_processor, 49 | do_sample=True, 50 | temperature=0.6, 51 | ) 52 | 53 | thinking_effort_response = tokenizer.decode( 54 | thinking_effort_output[0][input_ids.shape[-1]:], skip_special_tokens=True 55 | ) 56 | 57 | print("Thinking Effort Inference:", thinking_effort_response.strip()) 58 | -------------------------------------------------------------------------------- /thinking_effort_llamacpp_py.py: -------------------------------------------------------------------------------- 1 | def thinking_effort_processor(thinking_effort, end_thinking_token_id, scale_factor=2): 2 | """ 3 | Creates a callable logit-processor that modifies the probability of an 'end thinking' token 4 | based on the specified thinking effort. Typically used with llama-cpp or similar backends 5 | that support a custom logits_processor. 6 | 7 | Args: 8 | thinking_effort (float): 9 | A value between 0 and 1 that controls how much thinking to do. 10 | - Higher values (closer to 1) encourage more thinking by reducing the probability 11 | scaling of the end_thinking_token_id (i.e., minimal scaling). 12 | - Lower values (closer to 0) encourage less thinking by more strongly scaling up 13 | the probability of the end_thinking_token_id (i.e., large scaling). 14 | end_thinking_token_id (int): 15 | The token ID that marks the end of the "thinking" phase (e.g. ). On QwQ model, for example, this is 151668. 16 | scale_factor (float, optional): 17 | Controls the intensity of the scaling effect (default=2). 18 | - At thinking_effort=0.0, the end_thinking_token_id logit is multiplied by 19 | scale_factor^(1 - 0.0) = scale_factor (forcing the end token more likely). 20 | - At thinking_effort=1.0, the end_thinking_token_id logit is multiplied by 21 | scale_factor^(1 - 1.0) = 1 (no extra forcing). 22 | 23 | Returns: 24 | function: 25 | A logit processor function with signature (input_ids, logits) -> logits. 26 | 27 | Implementation Details: 28 | - The returned processor examines the most recent token in `input_ids`. If it matches 29 | `end_thinking_token_id`, we record that the end-of-thinking token has been generated 30 | and cease further scaling. 31 | - Because llama-cpp may pass `input_ids` as either a Python list or a NumPy array (possibly 32 | with shape (batch, seq_length)), the processor carefully extracts the last token as a 33 | Python int. This avoids errors when comparing it to `end_thinking_token_id`. 34 | - Once the end token is generated, the processor stops modifying logits altogether. 35 | """ 36 | # Compute how strongly to scale the end_thinking_token_id 37 | scale = scale_factor ** (1.0 - thinking_effort) 38 | 39 | # We store the "has generated end token" state in a list to make it mutable in this closure 40 | token_generated = [False] 41 | 42 | def processor(input_ids, logits): 43 | """ 44 | This inner function is the actual logit processor used at generation time. 45 | 46 | Args: 47 | input_ids: Could be a Python list of token IDs or a NumPy array of shape 48 | (seq_length,) or (batch_size, seq_length). 49 | logits: A 1D array or similar structure with the current token logits. 50 | 51 | Returns: 52 | Modified logits with the end_thinking_token scaled unless we've already 53 | seen that token. 54 | """ 55 | # If we've already generated the end token, do nothing further 56 | if token_generated[0]: 57 | return logits 58 | 59 | # Convert "input_ids" to a single integer for the last token 60 | if isinstance(input_ids, list): 61 | last_token_id = input_ids[-1] 62 | elif hasattr(input_ids, "shape"): 63 | # shape could be (seq_length,) or (batch, seq_length) 64 | if len(input_ids.shape) == 2: 65 | # Last row's last element 66 | last_token_id = input_ids[-1, -1] 67 | else: 68 | # e.g. shape == (seq_length,) 69 | last_token_id = input_ids[-1] 70 | # Convert from scalar array to Python int 71 | last_token_id = int(last_token_id) 72 | else: 73 | # fallback if it's just a scalar or another structure 74 | last_token_id = input_ids 75 | 76 | # If we've just generated the end_thinking_token, record that fact and do no more scaling 77 | if last_token_id == end_thinking_token_id: 78 | token_generated[0] = True 79 | return logits 80 | 81 | # Otherwise, multiply its logit by the scale factor 82 | logits[end_thinking_token_id] *= scale 83 | return logits 84 | 85 | return processor 86 | -------------------------------------------------------------------------------- /thinking_effort_transformers.py: -------------------------------------------------------------------------------- 1 | from transformers import LogitsProcessor 2 | import torch 3 | 4 | class ThinkingEffortProcessor(LogitsProcessor): 5 | """ 6 | A custom LogitsProcessor for Hugging Face Transformers that scales the logit for an 7 | "end-of-thinking" token based on a `thinking_effort` parameter—until that token is 8 | actually generated for each sequence, at which point it stops scaling for that sequence. 9 | 10 | Args: 11 | end_thinking_token_id (int): 12 | The special token ID representing the end-of-thinking marker (e.g. ). 13 | thinking_effort (float, optional): 14 | Controls how heavily to scale the end_thinking_token_id. Interpreted via: 15 | scale = scale_factor ** (1.0 - thinking_effort) 16 | - If thinking_effort=0, scale=scale_factor^1 => strongly boosts the end token 17 | (reducing thinking). 18 | - If thinking_effort=1, scale=scale_factor^0 => no scaling on the end token 19 | (normal chance, i.e. more thinking). 20 | - If thinking_effort>1, scale<1 => end token is suppressed (extensive thinking). 21 | Default is 1.0. 22 | scale_factor (float, optional): 23 | The base used in the exponent that determines how strongly to scale the end token. 24 | Default is 2. 25 | 26 | Behavior: 27 | - For each sequence in a batch, if the end_thinking_token_id has already appeared 28 | in previous steps of generation, no further scaling is applied for that sequence. 29 | - Otherwise, the logit for the end_thinking_token_id is multiplied by `scale`. 30 | 31 | 32 | Explanation: 33 | - The code runs at each generation step. For each sequence (row) in the batch: 34 | 1. If that sequence has already generated `end_thinking_token_id`, do nothing. 35 | 2. Otherwise, scale that token's logit by `scale = scale_factor ** (1.0 - thinking_effort)`. 36 | - This makes the end token more or less likely to appear, depending on `thinking_effort`. 37 | """ 38 | 39 | def __init__(self, end_thinking_token_id, thinking_effort=1.0, scale_factor=2): 40 | super().__init__() 41 | self.end_thinking_token_id = end_thinking_token_id 42 | self.thinking_effort = thinking_effort 43 | self.scale_factor = scale_factor 44 | # Track which sequences (by index) have already produced the end_thinking_token_id 45 | self.finished_sequences = set() 46 | 47 | def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor: 48 | """ 49 | Invoked at each generation step by the Transformers library. 50 | 51 | Args: 52 | input_ids (torch.LongTensor): 53 | The tokens generated so far, shape (batch_size, seq_length). 54 | scores (torch.FloatTensor): 55 | The current logits for the next token, shape (batch_size, vocab_size). 56 | 57 | Returns: 58 | torch.FloatTensor: 59 | The modified logits (same shape) with the end_thinking_token_id scaled 60 | for sequences that have not yet generated it. 61 | """ 62 | # Compute the scale factor from the current thinking_effort 63 | scale = self.scale_factor ** (1.0 - self.thinking_effort) 64 | 65 | batch_size = input_ids.size(0) 66 | # For each sequence in the batch, check if we've generated the end token before 67 | for i in range(batch_size): 68 | if i in self.finished_sequences: 69 | # Do not scale if we've already seen the end_thinking_token for this sequence 70 | continue 71 | 72 | # Check if this sequence contains the end_thinking_token_id already 73 | if (input_ids[i] == self.end_thinking_token_id).any(): 74 | # Mark that we've generated the end token for this sequence 75 | self.finished_sequences.add(i) 76 | # Don't scale the logit anymore 77 | continue 78 | 79 | # If we haven't encountered it yet, scale the logit for the end_thinking_token 80 | scores[i, self.end_thinking_token_id] *= scale 81 | 82 | return scores 83 | --------------------------------------------------------------------------------