I have created some code in order to simulate sierpinski's triangle. For this, in my code, I set it so that it can simulate any number of points on the triangle, but I've noticed that increasing this to a large number, it ends up with the turtle taking an amount of time that increases as it goes on.
I did a sample output with 100 000 dots and this is what I got
0% done
Time since last update: 0.0019948482513427734
5.0% done
Time since last update: 1.2903378009796143
10.0% done
Time since last update: 1.8589198589324951
15.000000000000002% done
Time since last update: 2.325822114944458
20.0% done
Time since last update: 2.9351391792297363
25.0% done
Time since last update: 3.4773638248443604
30.0% done
Time since last update: 4.152036190032959
35.0% done
Time since last update: 4.7314231395721436
40.0% done
Time since last update: 5.260996103286743
44.99999999999999% done
Time since last update: 5.988528490066528
49.99999999999999% done
Time since last update: 6.804485559463501
54.99999999999999% done
Time since last update: 7.768667221069336
60.0% done
Time since last update: 8.379971265792847
65.0% done
Time since last update: 8.995774745941162
70.0% done
Time since last update: 15.876121282577515
75.00000000000001% done
Time since last update: 17.292492151260376
80.00000000000001% done
Time since last update: 29.57323122024536
85.00000000000001% done
Time since last update: 65.96741080284119
90.00000000000003% done
Time since last update: 148.21749567985535
95.00000000000003% done
the code in question to run the main loop of the program is this
t = turtle.Turtle()
t.penup()
curr_time = time()
for x in range(iterations):
#The code will take a while should the triangle be large, so this will give its % completion
if x / iterations > percent_done:
print(percent_done * 100, r"% done", sep='')
percent_done += 0.05
window.update()
print(f"Time since last update:\t{time() - curr_time}")
curr_time = time()
c = determine_point(t, init_c)
make_point(t, c)
determine_point and make_point do not iterate at all, so I can't find a reason for turtle to slow down this considerably. Why does turtle slow down as more points are being created? (screen tracer has already been set to (0,0) and the code itself works as intended
make point:
def make_point(turt: turtle.Turtle, point):
'''
Basically does turt.goto(*point) where point is a list of length 2, then places a dot at that place
I did not use goto because I forgot what the function was, and instead of doing a 2 second google search to find out,
I did a 15 second google search to find out how to set the angle of the turtle, and use trigonometry and pythagorean
theorem in order to move the turt to the right position
'''
if point[0] != turt.xcor():
y = point[1] - turt.ycor()
x = point[0] - turt.xcor()
if x > 0:
turt.setheading(math.degrees(math.atan(y / x)))
else:
turt.setheading(math.degrees(math.pi + math.atan(y / x)))
else:
turt.setheading(0 if point[1] < turt.ycor() else 180)
turt.fd(pythag(turt.xcor(), turt.ycor(), point[0], point[1]))
turt.dot(3)
determine_point:
def determine_point(turt: turtle.Turtle, initial_cords):
'''
Returns the midpoint between the turt's current coordinates and a random one of the of the 3 starting points
'''
coord = initial_cords[random.randint(0, 2)]
result = []
result.append((coord[0] - turt.xcor()) / 2 + turt.xcor())
result.append((coord[1] - turt.ycor()) / 2 + turt.ycor())
return result
pythag:
def pythag(a, b, x, y):
'''
Does pythagorean theorem to find the length between 2 points
'''
return math.sqrt((x - a) ** 2 + (y - b) ** 2)
Although your code doesn't require more resources on each iteration, your program does because it is built atop libraries that do. This is true of both turtle and tkinter.
Although we think of turtle dot() as putting up dead ink, as opposed to stamp which can be selectively removed, to the underlying tkinter graphics, everything is live ink and adds to its (re)display lists.
I couldn't completely remove the increase in time for each iteration, but by optimizing the code, I believe I've reduced it to no longer being an issue. Some of my optimizations: put turtle into radians mode to avoid calls to math.degrees(); reduce calls into turtle, eg. by getting x and y in one step via position() rather than calling xcor() and ycor(); turn off turtle's undo buffer as it keeps a growing list of graphic commands:
from turtle import Screen, Turtle
from time import time
from random import choice
from math import sqrt, atan, pi
iterations = 100_000
init_c = [(300, -300), (300, 300), (-300, -300)]
def make_point(t, point):
'''
Basically do turtle.goto(*point) where point is a list of length 2,
then places a dot at that place
I did not use goto() because I forgot what the function was, and instead
of doing a 2 second google search to find out, I did a 15 second google
search to find out how to set the angle of the turtle, and use trigonometry
and pythagorean theorem in order to move the turtle to the right position
'''
x, y = t.position()
if point[0] != x:
dx = point[0] - x
dy = point[1] - y
if dx > 0:
t.setheading(atan(dy / dx))
else:
t.setheading(pi + atan(dy / dx))
else:
t.setheading(0 if point[1] < y else pi)
t.forward(pythag(x, y, point[0], point[1]))
t.dot(2)
def determine_point(t, initial_cords):
'''
Return the midpoint between the turtles's current coordinates
and a random one of the of the 3 starting points
'''
coord = choice(initial_cords)
x, y = t.position()
return [(coord[0] - x) / 2 + x, (coord[1] - y) / 2 + y]
def pythag(a, b, x, y):
'''
Do pythagorean theorem to find the length between 2 points
'''
return sqrt((x - a) ** 2 + (y - b) ** 2)
screen = Screen()
screen.tracer(False)
turtle = Turtle()
turtle.hideturtle()
turtle.setundobuffer(None)
turtle.radians()
turtle.penup()
fraction_done = 0
curr_time = time()
for iteration in range(iterations):
# The code will take a while should the triangle be large, so this will give its % completion
if iteration / iterations > fraction_done:
screen.update()
print(fraction_done * 100, r"% done", sep='')
fraction_done += 0.05
print(f"Time since last update:\t{time() - curr_time}")
curr_time = time()
make_point(turtle, determine_point(turtle, init_c))
screen.update()
screen.exitonclick()
CONSOLE
% python3 test.py
0% done
Time since last update: 0.024140119552612305
5.0% done
Time since last update: 1.2522082328796387
10.0% done
Time since last update: 1.619455099105835
15.000000000000002% done
Time since last update: 1.9793877601623535
20.0% done
...
Time since last update: 9.460317373275757
85.00000000000001% done
Time since last update: 10.161489009857178
90.00000000000003% done
Time since last update: 10.585438966751099
95.00000000000003% done
Time since last update: 11.479820966720581
This might be a more mathematical question, but I'm trying to get my head around how I can program an unbeatable AI for a ping pong game. From what I have read so far, it would be to simulate the trajectory of a ball when it is moving in the direction towards the AI Paddle.
In this game I have a ball and I can read its x and y position on the board, and then read it again in the next iteration which will allow me to calculate the velocity in the x and y direction.
But I'm not sure how to program how and where the ball will reach the AI paddle's goal position, and consider how many times the ball will bounce off the walls will requires me to use some geometry. But I can't get my head around it and how I will be programming it.
So far what I have thought of is the variables I've been given: the size of the table in x and y direction, the position of the ball "currently" and before in order to get its velocity in x and y direction. My first assumption is to find out a way to calculate whether the ball will hit the walls or the AI goal side instead?
There is a more direct way of doing this instead of repeated "raycasting":
def predict(x, y, vx, vy, h, b):
"""
:param x: ball x position
:param y: ball y position
:param vx: ball x velocity
:param vy: ball y velocity
:param h: the field height
:param b: the y position the prediction is for
:return: ball x position at y = b
"""
m = vy / vx # slope
c = -x * m + y # y-intercept
val = (m * b + c) % (2 * h)
return min(val, 2 * h - val)
Now, step by step
m = vy / vx # slope
c = -x * m + y # y-intercept
val = (m * b + c)
A simple linear function showing the ball's current path.
This works, but only if the ball never hits a side wall.
A Model
Imagine there were fields with the same height on both sides of the original one, stretching into infinity.
Now 'the number of bounces' has become 'the number of cells the ball travels'.
Additionally, if the number of bounces is even, the distance from the lower border of the cell it hits to the point of impact is the same as the height the actual ball would hit at in the real cell.
Therefore
(m * b + c) % (2 * h)
To cover odd bounces as well, you need to mirror the graph around h.
Here is a graphic explanation:
And since the irrelevant graph is the one with values above h, you take the minimum.
Possible Problems
In some languages, the % is a remainder operator, though not python.
If the predictions are negative in some cases add this.
val = ((m * b + c) % (2 * h) + 2 * h) % (2 * h)
This function depends on 'accurate' collision.
So if the bounces are handled in a way similar to this,
if y not in range(0, y_max):
vy *= -1
the predictions will be slightly off.
If you can change the core game, use
if y < 0:
y *= -1
vy *= -1
elif y > y_max:
y = 2 * y_max - y
vy *= -1
A divide by zero exception will be thrown if vx is 0, but since the ball will never hit a wall in this case, this should be handled by the ball movement logic.
Snippets are cool, but functions are better. I can't prove this works, but it seems to.
float pong_get_ball_endpoint(float xpos, float ypos, float xspeed, float yspeed)
{
// In the following, the fabs() mirrors it over the bottom wall. The fmod wraps it when it exceeds twice
// the top wall. If the ball ends up in the top half of the double height section, we reflect it back
auto deltaX = (xspeed > 0) ? (BAT2_X - xpos) : -(xpos - BAT1_X); // How far from ball to opponent bat
auto slope = yspeed / xspeed; // Rise over run, ie: deltaY per X
float newY = fmod(fabs(ypos + deltaX * slope), (2 * MATRIX_HEIGHT)); // New Y, but wrappped every 2*height
if (newY > MATRIX_HEIGHT) // If in top half, reflect to bottom
newY = 2 * MATRIX_HEIGHT - newY;
return newY;
}
I am trying to create a function for a homework assignment which draws a jagged mountain curve using turtles and recursion. The function is called jaggedMountain(x,y,c,t) where x x,y are end coordinates, c is a complexity constant, and t is the turtle object. I am trying to create an image like this:
def jaggedCurve(x,y,c,t):
t.pendown()
x1 = t.xcor() + x / 2
y1 = t.ycor() + y / 2
y1 = y + (random.uniform(0,c)-0.5) * (t.xcor() - x)
if (x1,y1) == (x,y):
return None
else:
jaggedCurve(x1,y1,c,t)
This crashes quickly as the base case never executes, the function is called 993 times, and the recursion depth is exceeded. I have been scratching my head with this for quite some time, are there any suggestions?
Initially, I see two issues with your code. The first is:
if (x1,y1) == (x,y):
Turtles wander a floating point plane, the odds of these being exactly equal is small. You're likely better off doing something like:
def distance(x1, y1, x2, y2):
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
...
if distance(x1, y1, x, y) < 1.0:
The second issue is that jaggedCurve() draws nothing nor returns anything that can be used for drawing. Somewhere you need to actually move the turtle to cause something to be drawn.
Finally, though it's hard to be certain without a value for c, my guess is even with the above changes you won't get you what you want. Good luck.
Very interesting problem!
My solution is to make a recursive function that draws a mountain curve given two end points. Randomly pick a x coordinate value that lies in between two end points and compute the range of possible y coordinate given the maximum possible slope and randomly pick a y value in between this range and do this recursively. When to end points are close enough, just draw the line between them. Here is the code:
MAX_SLOPE = 45
MIN_SLOPE = -45
MIN_HEIGHT = 0
def dist_squared(P1,P2):
return (P1[0]-P2[0])**2 + (P1[1]-P2[1])**2
def mountain(P1,P2):
if dist_squared(P1,P2) < 1:
turtle.goto(P2)
return
x1,y1 = P1
x2,y2 = P2
x3 = random.uniform(x1,x2)
y3_max = min((x3-x1)*math.tan(math.radians(MAX_SLOPE)) + y1, (x2-x3)*math.tan(-math.radians(MIN_SLOPE)) + y2)
y3_min = max((x3-x1)*math.tan(math.radians(MIN_SLOPE)) + y1, (x2-x3)*math.tan(-math.radians(MAX_SLOPE)) + y2)
y3_min = max(y3_min, MIN_HEIGHT)
y3 = random.uniform(y3_min,y3_max)
P3 = (x3, y3)
mountain(P1,P3)
mountain(P3,P2)
return
turtle.up()
turtle.goto(-400,0)
turtle.down()
mountain((-400,0),(400,0))
I know this was posted like 3 months ago, but hopefully this is helpful to someone that was also assigned this terrible problem 5 days before finals! Ha!
The struggle I had with this problem was not realizing that you only need to pass in one point. To get the point the turtle is starting at, you just use .xcor() and .ycor() that are included in the turtle library.
import turtle
import random
def mountain (x, y, complexity, turtleName):
if complexity == 0:
turtleName.setposition(x, y)
else:
x1 = (turtleName.xcor() + x)/2
y1 = (turtleName.ycor() + y)/2
y1 = y1 + (random.uniform(0, complexity) - 0.5) * (turtleName.xcor() - x)
complexity = complexity - 1
mountain(x1, y1, complexity, turtleName)
mountain(x, y, complexity, turtleName)
def main ():
#Gets input for first coordinate pair, splits, and assigns to variables
coordinate = str(input("Enter the coordinate pair, separated by a comma: "))
x, y = coordinate.split(',')
x = int(x)
y = int(y)
complexity = int(input("Enter the complexity: "))
while complexity < 0:
complexity = int(input("Input must be positive. Enter the complexity: "))
Bob = turtle.Turtle()
mountain(x, y, complexity, Bob)
main ()
I am new to Python, and currently having a rough time with turtle graphics. This is what I am trying to solve
On Turtellini (the planet where Python turtles live) the
transportation system propels turtles with a giant slingshot. A
particular turtle's original location (x0, y0) is (-180, -100). He is
then shot upward at an initial vertical velocity (vy) of 88 units per
second and a horizontal velocity (vx) of 20 units per second to the
right. He travels for 16 seconds. The acceleration due to gravity (g)
is 11 units per second squared. The the location of the turtle at a
given second (t) is calculated as follows: x = x0 + vx * t and y = y0
+ vy * t - g/2 * t2 . This program is to show how a turtle travels over this period of time.
The output should be like this:
Here is what I should do;
set up the constants (vertical velocity, horizontal velocity,
gravity) and variables (x and y coordinates) set up the turtle by
giving him a proper shape, putting his tail up, moving him to the
initial position, putting his tail down make a loop that repeats for
seconds 1 through 16 inclusive. in each iteration of the loop display
the the values of the x and y variables (in the shell window), move
the turtle to those coordinates, have the turtle stamp his shape,
calculate the new values for the x and y variables after the loop
terminates, move the turtle to the last calculated coordinates,
change his color, and stamp his shape, then wait for a mouse click
My code so far:
import turtle
def main():
wn = turtle.Screen()
turtellini = turtle.Turtle()
t = int(input("Blab blab blab: "))
x0 = -180
y0 = -100
vx = 20
vy = 88
g = 11
x = (float(x0 + vx * t))
y = (float(y0 + vy * t - g / 2 * t**2))
turtellini.color("black")
turtellini.shape("turtle")
turtellini.up()
turtellini.goto(-180,-100)
turtellini.down()
for i in range(1,16,1):
turtellini.stamp()
turtellini.forward(i)
turtellini.right(i)
print(x)
print(y)
if __name__ == "__main__":
main()
I know I am doing bad; but can anyone help me to solve this problem?
You seem to have most of the parts and pieces. The biggest issue I see is you didn't put your x,y calculation in the loop. The loop iteration variable i is really t in your motion equations. Each time you calculate a new x,y you simply move the turtle to that position:
import turtle
from math import pi, atan
x0, y0 = -180, -100 # initial location
vx, vy = 20.0, 88.0 # initial velocity in units per second
travel_time = 16 # seconds
g = 11.0 # acceleration due to gravity in units per second squared
turtellini = turtle.Turtle(shape='turtle', visible=False)
turtellini.penup()
turtellini.radians() # to make turtle compatible with math.atan()
turtellini.setheading(pi / 2) # straight up
turtellini.goto(x0, y0)
turtellini.pendown()
turtellini.showturtle()
turtellini.stamp()
for t in range(1, travel_time + 1):
x = x0 + vx * t
y = y0 + vy * t - g / 2 * t**2
turtellini.goto(x, y)
print(x, y)
angle = atan((vy * t - g * t**2) / (vx * t)) # a guess!
turtellini.setheading(angle)
turtellini.stamp()
turtle.exitonclick()
Unlike the gold standard image, I assumed the turtle was aerodynamic like a bullet and travelled head first through the flight. I don't know, and couldn't quickly find, the formula for the flight angle of a projectile so I guessed from the existing formulas:
I'm currently working on a pygame game and I need to place objects randomly on the screen, except they cannot be within a designated rectangle. Is there an easy way to do this rather than continuously generating a random pair of coordinates until it's outside of the rectangle?
Here's a rough example of what the screen and the rectangle look like.
______________
| __ |
| |__| |
| |
| |
|______________|
Where the screen size is 1000x800 and the rectangle is [x: 500, y: 250, width: 100, height: 75]
A more code oriented way of looking at it would be
x = random_int
0 <= x <= 1000
and
500 > x or 600 < x
y = random_int
0 <= y <= 800
and
250 > y or 325 < y
Partition the box into a set of sub-boxes.
Among the valid sub-boxes, choose which one to place your point in with probability proportional to their areas
Pick a random point uniformly at random from within the chosen sub-box.
This will generate samples from the uniform probability distribution on the valid region, based on the chain rule of conditional probability.
This offers an O(1) approach in terms of both time and memory.
Rationale
The accepted answer along with some other answers seem to hinge on the necessity to generate lists of all possible coordinates, or recalculate until there is an acceptable solution. Both approaches take more time and memory than necessary.
Note that depending on the requirements for uniformity of coordinate generation, there are different solutions as is shown below.
First attempt
My approach is to randomly choose only valid coordinates around the designated box (think left/right, top/bottom), then select at random which side to choose:
import random
# set bounding boxes
maxx=1000
maxy=800
blocked_box = [(500, 250), (100, 75)]
# generate left/right, top/bottom and choose as you like
def gen_rand_limit(p1, dim):
x1, y1 = p1
w, h = dim
x2, y2 = x1 + w, y1 + h
left = random.randrange(0, x1)
right = random.randrange(x2+1, maxx-1)
top = random.randrange(0, y1)
bottom = random.randrange(y2, maxy-1)
return random.choice([left, right]), random.choice([top, bottom])
# check boundary conditions are met
def check(x, y, p1, dim):
x1, y1 = p1
w, h = dim
x2, y2 = x1 + w, y1 + h
assert 0 <= x <= maxx, "0 <= x(%s) <= maxx(%s)" % (x, maxx)
assert x1 > x or x2 < x, "x1(%s) > x(%s) or x2(%s) < x(%s)" % (x1, x, x2, x)
assert 0 <= y <= maxy, "0 <= y(%s) <= maxy(%s)" %(y, maxy)
assert y1 > y or y2 < y, "y1(%s) > y(%s) or y2(%s) < y(%s)" % (y1, y, y2, y)
# sample
points = []
for i in xrange(1000):
x,y = gen_rand_limit(*blocked_box)
check(x, y, *blocked_box)
points.append((x,y))
Results
Given the constraints as outlined in the OP, this actually produces random coordinates (blue) around the designated rectangle (red) as desired, however leaves out any of the valid points that are outside the rectangle but fall within the respective x or y dimensions of the rectangle:
# visual proof via matplotlib
import matplotlib
from matplotlib import pyplot as plt
from matplotlib.patches import Rectangle
X,Y = zip(*points)
fig = plt.figure()
ax = plt.scatter(X, Y)
p1 = blocked_box[0]
w,h = blocked_box[1]
rectangle = Rectangle(p1, w, h, fc='red', zorder=2)
ax = plt.gca()
plt.axis((0, maxx, 0, maxy))
ax.add_patch(rectangle)
Improved
This is easily fixed by limiting only either x or y coordinates (note that check is no longer valid, comment to run this part):
def gen_rand_limit(p1, dim):
x1, y1 = p1
w, h = dim
x2, y2 = x1 + w, y1 + h
# should we limit x or y?
limitx = random.choice([0,1])
limity = not limitx
# generate x, y O(1)
if limitx:
left = random.randrange(0, x1)
right = random.randrange(x2+1, maxx-1)
x = random.choice([left, right])
y = random.randrange(0, maxy)
else:
x = random.randrange(0, maxx)
top = random.randrange(0, y1)
bottom = random.randrange(y2, maxy-1)
y = random.choice([top, bottom])
return x, y
Adjusting the random bias
As pointed out in the comments this solution suffers from a bias given to points outside the rows/columns of the rectangle. The following fixes that in principle by giving each coordinate the same probability:
def gen_rand_limit(p1, dim):
x1, y1 = p1Final solution -
w, h = dim
x2, y2 = x1 + w, y1 + h
# generate x, y O(1)
# --x
left = random.randrange(0, x1)
right = random.randrange(x2+1, maxx)
withinx = random.randrange(x1, x2+1)
# adjust probability of a point outside the box columns
# a point outside has probability (1/(maxx-w)) v.s. a point inside has 1/w
# the same is true for rows. adjupx/y adjust for this probability
adjpx = ((maxx - w)/w/2)
x = random.choice([left, right] * adjpx + [withinx])
# --y
top = random.randrange(0, y1)
bottom = random.randrange(y2+1, maxy)
withiny = random.randrange(y1, y2+1)
if x == left or x == right:
adjpy = ((maxy- h)/h/2)
y = random.choice([top, bottom] * adjpy + [withiny])
else:
y = random.choice([top, bottom])
return x, y
The following plot has 10'000 points to illustrate the uniform placement of points (the points overlaying the box' border are due to point size).
Disclaimer: Note that this plot places the red box in the very middle such thattop/bottom, left/right have the same probability among each other. The adjustment thus is relative to the blocking box, but not for all areas of the graph. A final solution requires to adjust the probabilities for each of these separately.
Simpler solution, yet slightly modified problem
It turns out that adjusting the probabilities for different areas of the coordinate system is quite tricky. After some thinking I came up with a slightly modified approach:
Realizing that on any 2D coordinate system blocking out a rectangle divides the area into N sub-areas (N=8 in the case of the question) where a valid coordinate can be chosen. Looking at it this way, we can define the valid sub-areas as boxes of coordinates. Then we can choose a box at random and a coordinate at random from within that box:
def gen_rand_limit(p1, dim):
x1, y1 = p1
w, h = dim
x2, y2 = x1 + w, y1 + h
# generate x, y O(1)
boxes = (
((0,0),(x1,y1)), ((x1,0),(x2,y1)), ((x2,0),(maxx,y1)),
((0,y1),(x1,y2)), ((x2,y1),(maxx,y2)),
((0,y2),(x1,maxy)), ((x1,y2),(x2,maxy)), ((x2,y2),(maxx,maxy)),
)
box = boxes[random.randrange(len(boxes))]
x = random.randrange(box[0][0], box[1][0])
y = random.randrange(box[0][1], box[1][1])
return x, y
Note this is not generalized as the blocked box may not be in the middle hence boxes would look different. As this results in each box chosen with the same probability, we get the same number of points in each box. Obviously the densitiy is higher in smaller boxes:
If the requirement is to generate a uniform distribution among all possible coordinates, the solution is to calculate boxes such that each box is about the same size as the blocking box. YMMV
I've already posted a different answer that I still like, as it is simple and
clear, and not necessarily slow... at any rate it's not exactly what the OP asked for.
I thought about it and I devised an algorithm for solving the OP's problem within their constraints:
partition the screen in 9 rectangles around and comprising the "hole".
consider the 8 rectangles ("tiles") around the central hole"
for each tile, compute the origin (x, y), the height and the area in pixels
compute the cumulative sum of the areas of the tiles, as well as the total area of the tiles
for each extraction, choose a random number between 0 and the total area of the tiles (inclusive and exclusive)
using the cumulative sums determine in which tile the random pixel lies
using divmod determine the column and the row (dx, dy) in the tile
using the origins of the tile in the screen coordinates, compute the random pixel in screen coordinates.
To implement the ideas above, in which there is an initialization phase in which we compute static data and a phase in which we repeatedly use those data, the natural data structure is a class, and here it is my implementation
from random import randrange
class make_a_hole_in_the_screen():
def __init__(self, screen, hole_orig, hole_sizes):
xs, ys = screen
x, y = hole_orig
wx, wy = hole_sizes
tiles = [(_y,_x*_y) for _x in [x,wx,xs-x-wx] for _y in [y,wy,ys-y-wy]]
self.tiles = tiles[:4] + tiles[5:]
self.pixels = [tile[1] for tile in self.tiles]
self.total = sum(self.pixels)
self.boundaries = [sum(self.pixels[:i+1]) for i in range(8)]
self.x = [0, 0, 0,
x, x,
x+wx, x+wx, x+wx]
self.y = [0, y, y+wy,
0, y+wy,
0, y, y+wy]
def choose(self):
n = randrange(self.total)
for i, tile in enumerate(self.tiles):
if n < self.boundaries[i]: break
n1 = n - ([0]+self.boundaries)[i]
dx, dy = divmod(n1,self.tiles[i][0])
return self.x[i]+dx, self.y[i]+dy
To test the correctness of the implementation, here it is a rough check that I
run on python 2.7,
drilled_screen = make_a_hole_in_the_screen((200,100),(30,50),(20,30))
for i in range(1000000):
x, y = drilled_screen.choose()
if 30<=x<50 and 50<=y<80: print "***", x, y
if x<0 or x>=200 or y<0 or y>=100: print "+++", x, y
A possible optimization consists in using a bisection algorithm to find the relevant tile in place of the simpler linear search that I've implemented.
It requires a bit of thought to generate a uniformly random point with these constraints. The simplest brute force way I can think of is to generate a list of all valid points and use random.choice() to select from this list. This uses a few MB of memory for the list, but generating a point is very fast:
import random
screen_width = 1000
screen_height = 800
rect_x = 500
rect_y = 250
rect_width = 100
rect_height = 75
valid_points = []
for x in range(screen_width):
if rect_x <= x < (rect_x + rect_width):
for y in range(rect_y):
valid_points.append( (x, y) )
for y in range(rect_y + rect_height, screen_height):
valid_points.append( (x, y) )
else:
for y in range(screen_height):
valid_points.append( (x, y) )
for i in range(10):
rand_point = random.choice(valid_points)
print(rand_point)
It is possible to generate a random number and map it to a valid point on the screen, which uses less memory, but it is a bit messy and takes more time to generate the point. There might be a cleaner way to do this, but one approach using the same screen size variables as above is here:
rand_max = (screen_width * screen_height) - (rect_width * rect_height)
def rand_point():
rand_raw = random.randint(0, rand_max-1)
x = rand_raw % screen_width
y = rand_raw // screen_width
if rect_y <= y < rect_y+rect_height and rect_x <= x < rect_x+rect_width:
rand_raw = rand_max + (y-rect_y) * rect_width + (x-rect_x)
x = rand_raw % screen_width
y = rand_raw // screen_width
return (x, y)
The logic here is similar to the inverse of the way that screen addresses are calculated from x and y coordinates on old 8 and 16 bit microprocessors. The variable rand_max is equal to the number of valid screen coordinates. The x and y co-ordinates of the pixel are calculated, and if it is within the rectangle the pixel is pushed above rand_max, into the region that couldn't be generated with the first call.
If you don't care too much about the point being uniformly random, this solution is easy to implement and very quick. The x values are random, but the Y value is constrained if the chosen X is in the column with the rectangle, so the pixels above and below the rectangle will have a higher probability of being chosen than pizels to the left and right of the rectangle:
def pseudo_rand_point():
x = random.randint(0, screen_width-1)
if rect_x <= x < rect_x + rect_width:
y = random.randint(0, screen_height-rect_height-1)
if y >= rect_y:
y += rect_height
else:
y = random.randint(0, screen_height-1)
return (x, y)
Another answer was calculating the probability that the pixel is in certain regions of the screen, but their answer isn't quite correct yet. Here's a version using a similar idea, calculate the probability that the pixel is in a given region and then calculate where it is within that region:
valid_screen_pixels = screen_width*screen_height - rect_width * rect_height
prob_left = float(rect_x * screen_height) / valid_screen_pixels
prob_right = float((screen_width - rect_x - rect_width) * screen_height) / valid_screen_pixels
prob_above_rect = float(rect_y) / (screen_height-rect_height)
def generate_rand():
ymin, ymax = 0, screen_height-1
xrand = random.random()
if xrand < prob_left:
xmin, xmax = 0, rect_x-1
elif xrand > (1-prob_right):
xmin, xmax = rect_x+rect_width, screen_width-1
else:
xmin, xmax = rect_x, rect_x+rect_width-1
yrand = random.random()
if yrand < prob_above_rect:
ymax = rect_y-1
else:
ymin=rect_y+rect_height
x = random.randrange(xmin, xmax)
y = random.randrange(ymin, ymax)
return (x, y)
If it's the generation of random you want to avoid, rather than the loop, you can do the following:
Generate a pair of random floating point coordinates in [0,1]
Scale the coordinates to give a point in the outer rectangle.
If your point is outside the inner rectangle, return it
Rescale to map the inner rectangle to the outer rectangle
Goto step 3
This will work best if the inner rectangle is small as compared to the outer rectangle. And it should probably be limited to only going through the loop some maximum number of times before generating new random and trying again.