├── .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 |
--------------------------------------------------------------------------------