python distance formula coordinate plane error - python

My goal is to make a circle shape out of lines in pygame, using random endpoints around the edge of a circle and a constant starting point (the middle of the circle). So I decided that I would give the pygame.draw.line function: screen, aRandomColor, startingPosition, and endingPosition as arguments. Ending position is a tuple containing a randomly generated x value, and a helper function will calculate the y value based on the radius of the circle. My first function calculates the y value like this:
import math
import random
def findY(pos1, pos2, distance, bothValues=False):
p1 =pos1
p2 = pos2
x1 = float(p1[0])
y1 = float(p1[1])
x2 = float(p2[0])
d = float(distance)
y2 = y1 - math.sqrt(d**2 - (x1-x2)**2)
_y2 = y1 + math.sqrt(d**2 - (x1-x2)**2)
if bothValues==True:
return y2, _y2
else:
return y2
and the line drawer:
width = 500
height = 500
def randLine(surface, color=rand, start=rand, end=rand,length=rand):
if start==rand:
start = randPos()
if end==rand:
end = randPos()
if color==rand:
color = randColor()
if length != rand:
end_x = float(random.randint(0,width))
end_pos = (end_x, "y")
y2=findMissing(start_pos, end_pos,l,bothValues=True)
a = random.randint(0,1)
if a==0:
y2 = float(y2[0])
else:
y2 = float(y2[1])
lst = list(end_pos)
lst[1] = y2
end_pos = tuple(lst)
pygame.draw.line(surface, color, start_pos, end_pos)
Then:
drawRandLine(screen,start=(200,200),lenght=100)
(the other functions that those ones called like randPos aren't the problem). This for some reason generated an error that I diagnosed as the value inside the math.sqrt() was a negative number. But that can't happen, since every value in there is raised to power of 2, and thats what I'm confused about. So I changed the value inside math.sqrt() to its absolute value. This made the function not raise any errors, but the circle drawn looked like this:
I know that pygame's coordinate plane's y values upside down, but should that make a difference?

One way of getting a a uniform distribution of angles would be to generate a random angle theta between 0 and 2 * math.pi, and use trigonometry to find the co-ordinates of the end point of the line:
def drawRandLineTrig(start_pos, length):
theta = random.rand() * 2 * math.pi
end_pos = (start_pos[0] + length*math.cos(theta), start_pos[1] + length*math.sin(theta))
# ...

I could Do:
def randPos(origin=(0,0), xdis=width, ydis=height):
randx = random.randint(float(origin[0])-xdis,float(origin[0])+xdis)
randy = random.randint(float(origin[1])-ydis,float(origin[1])+ydis)
return (randx, randy)
def drawRandLine(surface, color=random, start_pos=random, end_pos=random,l=random):
if start_pos==random:
start_pos = randPos()
if end_pos==random:
end_pos = randPos()
if color==random:
color = randColor()
if l != random:
b = random.randint(0,1)
l=float(l)
origin = start_pos
dis = l
if b==0:
end_x = float(random.randint((origin[0]-dis),(origin[0]+dis)))
end_pos = (end_x, "y")
y2=findMissing(start_pos, end_pos,l,bothValues=True)
a = random.randint(0,1)
if a==0:
y2 = float(y2[0])
else:
y2 = float(y2[1])
lst = list(end_pos)
lst[1] = y2
end_pos = tuple(lst)
else:
end_y = float(random.randint((origin[1]-dis),(origin[1]+dis)))
end_pos = ("x", end_y)
x2=findMissing(start_pos, end_pos,l,bothValues=True)
a = random.randint(0,1)
if a==0:
x2 = float(x2[0])
else:
x2 = float(x2[1])
lst = list(end_pos)
lst[0] = x2
end_pos = tuple(lst)
pygame.draw.line(surface, color, start_pos, end_pos)
Which fixes the sqrt of a negative problem, and places an even distribution of lines by either calculating the x OR the y, and setting their limits to the specified length, to avoid the strange length of some of the lines.

Related

Is it possible to know the coordinates of a line at a specific point in time in python Tkinter

This is the game that I have made I want the circle that has been generated to move across the line without actually touching it, the circle moves to the right and downwards when no keypresses are made and is supposed to jump when the spacebar is pressed, when it collides with the line (logic I made for hitreg) it will restart with a brand new line and the circle at the starting position. When it goes from one end to the other it'll restart with a new line that has more divisions so that the game is difficult as you progress through the levels. The only issue I am having right now is that I am not able to start up the game correctly without it exiting the code immediately and I think the reason might be that the coordinates I have calculated are not accurate enough and sometimes the circle crosses the line and still doesn't register a collision.
Code for the game
from tkinter import *
from random import randint as rand
import keyboard, math
window = Tk()
window.geometry("1440x720")
canvas = Canvas(window, width=1440, height=720,
bg="white")
l1, l2, l3 = [], [], []
x = 1
b = canvas.create_oval(0, 300, 10, 300 + 60)
c = b
def genLine():
global x
f = 0
z = 360
y = 1440 / x
while x != 0:
ran = rand(300, 420)
u = canvas.create_line(f, z, y, ran)
r = canvas.coords(u)
l1.append(r)
f = y
x -= 1
y += y
z = ran
def hitReg():
global l2, l3
for i in l1:
grad = ((i[3] - i[1]) / (i[2] - i[0]))
l2.append(grad)
for i in l2:
for f in l1:
x = 0
length = f[2] - f[0]
while x != length:
point = x * i + f[1]
l3.append(math.ceil( point))
x += 0.25
def move_b():
global l3, x
canvas.move(c, 1, 1)
y = canvas.coords(c)
if keyboard.is_pressed('space'):
canvas.move(b, 1, -5)
elif y[1] in l3 or y[3] in l3:
exit()
elif y[2] >= 1440:
x += 1
genLine()
hitReg()
move_b()
else:
pass
window.after(10, move_b)
genLine()
hitReg()
move_b()
canvas.pack()
window.mainloop()
Main problem is
elif y[1] in l3 or y[3] in l3:
because l3 has all y values which are on line and it compares y[1] and y[3] with all y values but it should compare only with the nearest - with point which has the same x as center of oval.
I was thinking to use canvas.find_overlapping() or canvas.find_closest() but it would need to create small rectange or small oval for every hit point on line.
Finally I created hit points every 1px instead of 0.25px so I could put them on list and easily get
point_y = points[ int(oval_center_x) ]
and later check oval_y1 < point_y < oval_y2.
I also used window.bind("<space>", function) to move oval so I don't need extra module keyboard. Besides, I use Linux and keyboard on Linux needs admin privilege.
I also changed names of variables and functions to make them more readable.
As for exit() I created function reset() which sets level, removes all lines canvas.delete(*lines), moves oval to beginning canvas.moveto(oval, 0, 330), and generates new line (using value level) and generates hit points. Using reset(level+1) I can generate next level, and using reset(1) I can go back to first level.
import tkinter as tk # PEP8: `import *` is not preferred
from random import randint # PEP8: every module in separted line
# --- functions --- # PEP8: all functions directly after imports
# PEP8: `lower_case_names` for functions and variables
def generate_lines(level):
regions = []
lines = []
x1 = 0
y1 = 360
step = 1440 / level
x2 = step
for i in range(level):
y2 = randint(300, 420) # 360-60, 360+60
region = (x1, y1, x2, y2)
regions.append(region)
line = canvas.create_line(x1, y1, x2, y2)
lines.append(line)
x1 = x2
y1 = y2
x2 += step
return lines, regions
def generate_hit_points(regions):
points = []
for x1,y1,x2,y2 in regions:
steps = int(x2 - x1)
dy = (y2 - y1) / steps
y = y1
for _ in range(steps): # cant use `range(y1, y2, dy)` because `dy` is `float`, not `int`
points.append(y) # I don't need `x`
y += dy
return points
def move_oval():
canvas.move(oval, 1, 1)
x1, y1, x2, y2 = canvas.coords(oval)
oval_center_x = int(x1+10)
point_y = points[oval_center_x]
#print(y1, '<', int(point_y), '<', y2, '?')
#if y1 >= point_y or point_y >= y2: # less readable
if not (y1 < point_y < y2): # more readable
print('end')
reset(1)
elif x2 >= 1440:
print('next level')
reset(level+1)
window.after(25, move_oval)
def on_press_space(event):
canvas.move(oval, 0, -15)
def reset(new_level):
# all globals moved to one function
global lines
global points
global level
level = new_level
canvas.delete(*lines) # remove all lines
canvas.moveto(oval, 0, 330) # move oval to the beginning
lines, regions = generate_lines(level)
points = generate_hit_points(regions)
# --- main ---
lines = [] # PEP8: every variable in separted line
points = [] # PEP8: every variable in separted line
level = 1
window = tk.Tk()
window.geometry("1440x720")
canvas = tk.Canvas(window, width=1440, height=720, bg="white")
canvas.pack()
oval = canvas.create_oval(0, 360-30, 10, 360+30)
window.bind('<space>', on_press_space)
window.bind('q', lambda event:window.destroy())
reset(1)
# start moving
move_oval()
window.mainloop()
PEP 8 -- Style Guide for Python Code

Why is My Python Turtle Screen not responding

I am trying to create a collision detection system between a turtle and another turtle's line. The problem is that whenever i run the program, the turtle screen does not respond.I am using Pycharm and Python. Please Help!
import turtle
Screen = turtle.Screen()
P1 = turtle.Turtle()
P2 = turtle.Turtle()
Screen.screensize(100, 100)
x1 = []
x2 = []
y1 = []
y2 = []
P1.penup()
P1.setheading(180)
P2.setheading(90)
P1.goto(100, 50)
P2.penup()
P2.goto(50, 0)
P2.pendown()
P1.pendown()
n = 0
Num = 0
XC = P2.position()[0]
YC = P2.position()[1]
x1.append(XC)
y1.append(YC)
while Num == 0:
XC = P2.position()[0]
YC = P2.position()[1]
x1[n] = XC
y1[n] = YC
if P1.heading() is 180:
XC = P2.position()[0]
YC = P2.position()[1]
x2[n] = XC
y2[n] = YC
P1.position()
XC1 = P1.position()[0]
YC1 = P1.position()[1]
for x in range(0, n):
for z in range(x1.index(x), x2.index(x)):
if abs(z-YC1)<10:
print("Found")
P2.forward(1)
P1.forward(0.5)
There are a few problems with that code. A major one is this line:
if P1.heading() is 180:
The is operator tests identity, i.e., it's True if two expressions evaluate to the same object. You should not use it to test if two expressions have the same value. In this case, P1.heading() returns the float object with a value of 180.0, so there's no way it could be the same object as the integer object with value 180. Thus your if block is never entered. And since the main commands that move the turtles are at the end of the if block, the turtles don't move once you enter the while loop.
Also, you are using n to index into your lists, but you never update n from zero, so all of the coordinates that you want to save are getting written into the first items of the lists.
However, if you did update n you'd run into another problem: you'd be attempting to index list items that don't exist, since x1 and y1 are of length one, and x2 and y2 are of length zero.
Anyway, here's a simplified version of your code that does update the lists correctly, and does simple collision detection. It only detects exact collision, not approximate collision, but it should get you going in the right direction.
import turtle
Screen = turtle.Screen()
Screen.screensize(100, 100)
P1 = turtle.Turtle()
P2 = turtle.Turtle()
x1 = []
x2 = []
y1 = []
y2 = []
P1.penup()
P1.setheading(180)
P2.setheading(90)
P1.goto(100, 50)
P2.penup()
P2.goto(50, 0)
P2.pendown()
P1.pendown()
while True:
print(P1.heading() is 180)
XC = P2.position()[0]
YC = P2.position()[1]
x2.append(XC)
y2.append(YC)
XC = P1.position()[0]
YC = P1.position()[1]
x1.append(XC)
y1.append(YC)
if XC in x2 and YC in y2:
print("Found")
P2.forward(1)
P1.forward(0.5)
turtle.done()
There are many errors in your code.
First,
if P1.heading() is 180:
should be
if P1.heading() == 180:
In your loop you don't change your Num var.
You are trying to access an index that was not defined
x2[n] = XC
y2[n] = YC
Your logic doesn't make much sense in that for loop either.
Here, I made some corrections and I included a timer so you can see how your turtles are moving. This should help you to visualize what you are doing
import time
import turtle
Screen = turtle.Screen()
P1 = turtle.Turtle()
P2 = turtle.Turtle()
Screen.screensize(100, 100)
P1.penup()
P1.setheading(180)
P2.setheading(90)
P1.goto(100, 50)
P2.penup()
P2.goto(50, 0)
P2.pendown()
P1.pendown()
time.sleep(1)
n = 100
if P1.heading() == 180:
for x in range(0, n):
print(P1.position())
print(P2.position())
if abs(P1.position()[1] - P2.position()[1]) > 10:
time.sleep(.3)
P2.forward(1)
P1.forward(.5)
else:
print('Found')
break

Optimisation of Shadow Casting Python

I have been working on a Shadow Caster for a small RPG I'm doing.
The trouble I have is that when I use it in my game it is just way way way to slow and induces a horrible lag.
Please do not be too frighten by the lenght of the post. It is fairly straightforward but so that you can run the code I included all the Bresenham's algorithms as well.
The principle is as follow:
- make a black surface
- define a light source with a position and a radius.
- get all the points on the circumference of the circle defined by this position and radius using Bresenham's Circle Algorithm.
- for each point along the circumference draw a ligne from the position of the light source using Bresenham's Line Algorithm.
- then iterate over the points of the line and check if they collide with every obstacle displayed on the screen.
- If there is no collision draw a WHITE circle centered on that point with a radius of 10 pixels or so.
- If there is a collision move on to the next point along the circle circumference.
- finally blit the surface with all the white circles on a surface which has a transparency value of 100 for the black color and a full transparency for the WHITE color.
So far I have attempted the following:
Which did reduce the lag:
- restrict the obstacle list to the ones displayed on the screen
- consider the screen edges as obstacles to reduce the iteration of area not visible.
- iterate only over every 3 points around the circle and 12 points along the lines.
Which didn't change anything:
- using ellipses going from the light source to the edge of the range or the obstacle instead of lots of circles along the line. The problem was that I had to redraw surface for each ellipse and then rotate the whole lot.
If you have any suggestions on how to make this more efficient I would be happy to here then.
Bresenham's Line Algo:
def get_line(start, end):
"""Bresenham's Line Algorithm
Produces a list of tuples from start and end
>>> points1 = get_line((0, 0), (3, 4))
>>> points2 = get_line((3, 4), (0, 0))
>>> assert(set(points1) == set(points2))
>>> print points1
[(0, 0), (1, 1), (1, 2), (2, 3), (3, 4)]
>>> print points2
[(3, 4), (2, 3), (1, 2), (1, 1), (0, 0)]
"""
# Setup initial conditions
x1, y1 = start
x2, y2 = end
dx = x2 - x1
dy = y2 - y1
# Determine how steep the line is
is_steep = abs(dy) > abs(dx)
# Rotate line
if is_steep:
x1, y1 = y1, x1
x2, y2 = y2, x2
# Swap start and end points if necessary and store swap state
swapped = False
if x1 > x2:
x1, x2 = x2, x1
y1, y2 = y2, y1
swapped = True
# Recalculate differentials
dx = x2 - x1
dy = y2 - y1
# Calculate error
error = int(dx / 2.0)
ystep = 1 if y1 < y2 else -1
# Iterate over bounding box generating points between start and end
y = y1
points = []
for x in range(x1, x2 + 1):
coord = (y, x) if is_steep else (x, y)
points.append(coord)
error -= abs(dy)
if error < 0:
y += ystep
error += dx
# Reverse the list if the coordinates were swapped
if swapped:
points.reverse()
return points
Bresenham's Circle Algo:
def get_circle((dx,dy),radius):
"Bresenham complete circle algorithm in Python"
# init vars
switch = 3 - (2 * radius)
points = set()
x = 0
y = radius
# first quarter/octant starts clockwise at 12 o'clock
while x <= y:
# first quarter first octant
points.add((x,-y))
# first quarter 2nd octant
points.add((y,-x))
# second quarter 3rd octant
points.add((y,x))
# second quarter 4.octant
points.add((x,y))
# third quarter 5.octant
points.add((-x,y))
# third quarter 6.octant
points.add((-y,x))
# fourth quarter 7.octant
points.add((-y,-x))
# fourth quarter 8.octant
points.add((-x,-y))
if switch < 0:
switch = switch + (4 * x) + 6
else:
switch = switch + (4 * (x - y)) + 10
y = y - 1
x = x + 1
offset_points = set()
for pt in points:
offset_points.add((pt[0]+dx,pt[1]+dy))
return offset_points
def shadow_gen(shadow_surf,source,cir_pt,obstacles):
line_points = get_line(source.pos,cir_pt)
for line_pt in line_points[0::12]:
for obs in obstacles:
pygame.draw.circle(shadow_surf, WHITE, line_pt, 20, 0) #radius to 5px and 0 to fill the circle
if obs.rect.collidepoint(line_pt) or pygame.Rect(0,0,500,500).collidepoint(line_pt) == False:
return
My Classes for the light sources, obstacles and shadow mask:
class Obstacle(object):
def __init__(self,x,y):
self.surf = pygame.Surface((150,150))
self.rect = pygame.Rect((x,y),(150,150))
self.surf.fill(pygame.color.Color('blue'))
class Light_Source(object):
def __init__(self,x,y,range_):
self.range = range_
self.pos = (x,y)
class Night_Mask(object):
def __init__(self):
self.surf = pygame.Surface((500,500)) #Screenwidth and height
self.alpha = 100
self.light_sources = []
'''setting initial alpha and colorkey'''
self.surf.set_colorkey(WHITE)
self.surf.set_alpha(self.alpha)
def apply_shadows(self, obstacles):
shadow_surf = pygame.Surface((500,500))
for source in self.light_sources:
circle_pts = list(get_circle(source.pos,source.range))
for cir_pt in circle_pts[0::3]:
shadow_gen(shadow_surf,source,cir_pt,obstacles)
self.surf.blit(shadow_surf, (0, 0))
The shadow generation functions which allows me to break out of both line and obstacle loop without using an exception in my apply_shadows method of the Night_Mask class:
def shadow_gen(shadow_surf,source,cir_pt,obstacles):
line_points = get_line(source.pos,cir_pt)
for line_pt in line_points[0::12]:
for obs in obstacles:
pygame.draw.circle(shadow_surf, WHITE, line_pt, 20, 0) #radius to 5px and 0 to fill the circle
if obs.rect.collidepoint(line_pt) or pygame.Rect(0,0,500,500).collidepoint(line_pt) == False:
return
And finally the main pygame example loop to run all of the above:
pygame.init()
screen = pygame.display.set_mode((500, 500))
bg = pygame.Surface((500,500))
bg.fill(pygame.color.Color('yellow'))
ob_a = Obstacle(75,80)
ls = Light_Source(75,75,300)
night_m = Night_Mask()
night_m.light_sources.extend([ls])
while True:
screen.fill(pygame.color.Color('black'))
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
ls.pos = pygame.mouse.get_pos()
night_m.apply_shadows([ob_a])
screen.blit(bg, (0, 0))
screen.blit(ob_a.surf,ob_a.rect)
screen.blit(night_m.surf, (0, 0))
pygame.display.flip()
Here is the entire code from start to finish for an easy copy paste:
import pygame
import sys
WHITE = (255,255,255)
'''FUNCTIONS'''
def get_line(start, end):
"""Bresenham's Line Algorithm
Produces a list of tuples from start and end
>>> points1 = get_line((0, 0), (3, 4))
>>> points2 = get_line((3, 4), (0, 0))
>>> assert(set(points1) == set(points2))
>>> print points1
[(0, 0), (1, 1), (1, 2), (2, 3), (3, 4)]
>>> print points2
[(3, 4), (2, 3), (1, 2), (1, 1), (0, 0)]
"""
# Setup initial conditions
x1, y1 = start
x2, y2 = end
dx = x2 - x1
dy = y2 - y1
# Determine how steep the line is
is_steep = abs(dy) > abs(dx)
# Rotate line
if is_steep:
x1, y1 = y1, x1
x2, y2 = y2, x2
# Swap start and end points if necessary and store swap state
swapped = False
if x1 > x2:
x1, x2 = x2, x1
y1, y2 = y2, y1
swapped = True
# Recalculate differentials
dx = x2 - x1
dy = y2 - y1
# Calculate error
error = int(dx / 2.0)
ystep = 1 if y1 < y2 else -1
# Iterate over bounding box generating points between start and end
y = y1
points = []
for x in range(x1, x2 + 1):
coord = (y, x) if is_steep else (x, y)
points.append(coord)
error -= abs(dy)
if error < 0:
y += ystep
error += dx
# Reverse the list if the coordinates were swapped
if swapped:
points.reverse()
return points
def get_circle((dx,dy),radius):
"Bresenham complete circle algorithm in Python"
# init vars
switch = 3 - (2 * radius)
points = set()
x = 0
y = radius
# first quarter/octant starts clockwise at 12 o'clock
while x <= y:
# first quarter first octant
points.add((x,-y))
# first quarter 2nd octant
points.add((y,-x))
# second quarter 3rd octant
points.add((y,x))
# second quarter 4.octant
points.add((x,y))
# third quarter 5.octant
points.add((-x,y))
# third quarter 6.octant
points.add((-y,x))
# fourth quarter 7.octant
points.add((-y,-x))
# fourth quarter 8.octant
points.add((-x,-y))
if switch < 0:
switch = switch + (4 * x) + 6
else:
switch = switch + (4 * (x - y)) + 10
y = y - 1
x = x + 1
offset_points = set()
for pt in points:
offset_points.add((pt[0]+dx,pt[1]+dy))
return offset_points
def shadow_gen(shadow_surf,source,cir_pt,obstacles):
line_points = get_line(source.pos,cir_pt)
for line_pt in line_points[0::12]:
for obs in obstacles:
pygame.draw.circle(shadow_surf, WHITE, line_pt, 20, 0) #radius to 5px and 0 to fill the circle
if obs.rect.collidepoint(line_pt) or pygame.Rect(0,0,500,500).collidepoint(line_pt) == False:
return
'''CLASSES'''
class Obstacle(object):
def __init__(self,x,y):
self.surf = pygame.Surface((150,150))
self.rect = pygame.Rect((x,y),(150,150))
self.surf.fill(pygame.color.Color('blue'))
class Light_Source(object):
def __init__(self,x,y,range_):
self.range = range_
self.pos = (x,y)
class Night_Mask(object):
def __init__(self):
self.surf = pygame.Surface((500,500)) #Screenwidth and height
self.alpha = 100
self.light_sources = []
'''setting initial alpha and colorkey'''
self.surf.set_colorkey(WHITE)
self.surf.set_alpha(self.alpha)
def apply_shadows(self, obstacles):
shadow_surf = pygame.Surface((500,500))
for source in self.light_sources:
circle_pts = list(get_circle(source.pos,source.range))
for cir_pt in circle_pts[0::3]:
shadow_gen(shadow_surf,source,cir_pt,obstacles)
self.surf.blit(shadow_surf, (0, 0))
'''MAIN GAME'''
pygame.init()
screen = pygame.display.set_mode((500, 500))
bg = pygame.Surface((500,500))
bg.fill(pygame.color.Color('yellow'))
ob_a = Obstacle(75,80)
ls = Light_Source(75,75,300)
night_m = Night_Mask()
night_m.light_sources.extend([ls])
while True:
screen.fill(pygame.color.Color('black'))
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
ls.pos = pygame.mouse.get_pos()
night_m.apply_shadows([ob_a])
screen.blit(bg, (0, 0))
screen.blit(ob_a.surf,ob_a.rect)
screen.blit(night_m.surf, (0, 0))
pygame.display.flip()
Your lag issue appears to be coming from the method Night_Mask.apply_shadows(self, obstacles). This appears to be due to the pure amount of iterations the nested for loop needs to go through.
Reducing the value of range_ in the constructor of Light_Source(x, y, range_) reduces the lag by reducing the aforementioned methods iterations, but the visual effect is worse. I found that the fps started to really drop for me after setting the variable past ~65-70.
There is a Pygame graphics library, that handles shadows very well.
Link to the page:http://pygame.org/project-Pygame+Advanced+Graphics+Library-660-4586.html
Direct download for version 8.1.1 from site:link
This is the description of the library from the site:
This is an all purpose graphics library for easily creating complicated effects quickly, and with a minimum of code. Run the very well commented examples, each less than a page long (not counting comments), and learn how to make complicated effects like shadows and antialiasing.
Here is an image from the page showing an example of shadows.
I downloaded and tested the library, and it works very well. I tested on Pygame1.9.2a0 for python 3.4
I believe this to be the easiest fix for your problem, and should help you with future projects as well. I hope this helps.

Generate random number outside of range in python

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.

Drawing diagonal lines on an image

Hi im trying to draw diagonal lines across an image top right to bottom left here is my code so far.
width = getWidth(picture)
height = getHeight(picture)
for x in range(0, width):
for y in range(0, height):
pixel = getPixel(picture, x, y)
setColor(pixel, black)
Thanks
Most graphic libraries have some way to draw a line directly.
In JES there is the addLine function, so you could do
addLine(picture, 0, 0, width, height)
If you're stuck with setting single pixels, you should have a look at Bresenham Line Algorithm, which is one of the most efficient algorithms to draw lines.
A note to your code: What you're doing with two nested loops is the following
for each column in the picture
for each row in the current column
set the pixel in the current column and current row to black
so basically youre filling the entire image with black pixels.
EDIT
To draw multiple diagonal lines across the whole image (leaving a space between them), you could use the following loop
width = getWidth(picture)
height = getHeight(picture)
space = 10
for x in range(0, 2*width, space):
addLine(picture, x, 0, x-width, height)
This gives you an image like (the example is hand-drawn ...)
This makes use of the clipping functionality, most graphics libraries provide, i.e. parts of the line that are not within the image are simply ignored. Note that without 2*width (i.e. if x goes only up to with), only the upper left half of the lines would be drawn...
I would like to add some math considerations to the discussion...
(Just because it is sad that JES's addLine function draws black lines only and is quite limited...)
Note : The following code uses the Bresenham's Line Algorithm pointed out by MartinStettner (so thanks to him).
The Bresenham's line algorithm is an algorithm which determines which order to form a close approximation to a straight line between two given points. Since a pixel is an atomic entity, a line can only be drawn on a computer screen by using some kind of approximation.
Note : To understand the following code, you will need to remember a little bit of your basic school math courses (line equation & trigonometry).
Code :
# The following is fast implementation and contains side effects...
import random
# Draw point, with check if the point is in the image area
def drawPoint(pic, col, x, y):
if (x >= 0) and (x < getWidth(pic)) and (y >= 0) and (y < getHeight(pic)):
px = getPixel(pic, x, y)
setColor(px, col)
# Draw line segment, given two points
# From Bresenham's line algorithm
# http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
def drawLine(pic, col, x0, y0, x1, y1):
dx = abs(x1-x0)
dy = abs(y1-y0)
sx = sy = 0
#sx = 1 if x0 < x1 else -1
#sy = 1 if y0 < y1 else -1
if (x0 < x1):
sx = 1
else:
sx = -1
if (y0 < y1):
sy = 1
else:
sy = -1
err = dx - dy
while (True):
drawPoint(pic, col, x0, y0)
if (x0 == x1) and (y0 == y1):
break
e2 = 2 * err
if (e2 > -dy):
err = err - dy
x0 = x0 + sx
if (x0 == x1) and (y0 == y1):
drawPoint(pic, col, x0, y0)
break
if (e2 < dx):
err = err + dx
y0 = y0 + sy
# Draw infinite line from segment
def drawInfiniteLine(pic, col, x0, y0, x1, y1):
# y = m * x + b
m = (y0-y1) / (x0-x1)
# y0 = m * x0 + b => b = y0 - m * x0
b = y0 - m * x0
x0 = 0
y0 = int(m*x0 + b)
# get a 2nd point far away from the 1st one
x1 = getWidth(pic)
y1 = int(m*x1 + b)
drawLine(pic, col, x0, y0, x1, y1)
# Draw infinite line from origin point and angle
# Angle 'theta' expressed in degres
def drawInfiniteLineA(pic, col, x, y, theta):
# y = m * x + b
dx = y * tan(theta * pi / 180.0) # (need radians)
dy = y
if (dx == 0):
dx += 0.000000001 # Avoid to divide by zero
m = dy / dx
# y = m * x + b => b = y - m * x
b = y - m * x
# get a 2nd point far away from the 1st one
x1 = 2 * getWidth(pic)
y1 = m*x1 + b
drawInfiniteLine(pic, col, x, y, x1, y1)
# Draw multiple parallele lines, given offset and angle
def multiLines(pic, col, offset, theta, randOffset = 0):
# Range is [-2*width, 2*width] to cover the whole surface
for i in xrange(-2*getWidth(pic), 2*getWidth(pic), offset):
drawInfiniteLineA(pic, col, i + random.randint(0, randOffset), 1, theta)
# Draw multiple lines, given offset, angle and angle offset
def multiLinesA(pic, col, offsetX, offsetY, theta, offsetA):
j = 0
# Range is [-2*width, 2*width] to cover the whole surface
for i in xrange(-2*getWidth(pic), 2*getWidth(pic), offsetX):
drawInfiniteLineA(pic, col, i, j, theta)
j += offsetY
theta += offsetA
file = pickAFile()
picture = makePicture(file)
color = makeColor(0, 65, 65) #pickAColor()
#drawline(picture, color, 10, 10, 100, 100)
#drawInfiniteLine(picture, color, 10, 10, 100, 100)
#drawInfiniteLineA(picture, color, 50, 50, 135.0)
#multiLines(picture, color, 20, 56.0)
#multiLines(picture, color, 10, 56.0, 15)
multiLinesA(picture, color, 10, 2, 1.0, 1.7)
show(picture)
Output (Painting by Pierre Soulages) :
Hope this gave some fun and ideas to JES students... And to others as well...
Where does your picture object comes from? What is it? What is not working so far? And what library for image access are you trying to use? (I mean, where do you get, or intend to get "getWidth, getHeight, getPixel, setColor) from?
I think no library that gives you a "pixel" as a whole object which can be used in a setColor call exists, and if it does, it would be the slowest thing in the World - maybe in the galaxy.
On the other hand, if these methods did exist and your Picture, the code above would cover all the image in black - you are getting all possible "y" values (from 0 to height) inside all possible x values (from 0 to width) of the image, and coloring each Black.
Drawing a line would require you to change x, and y at the same time, more like:
(using another "imaginary library", but one more plausible:
for x, y in zip(range(0, width), range(0, height)):
picture.setPixel((x,y), Black) )
This would sort of work, but the line would not be perfect unless the image was perfectly square - else it would skip pixels in the widest direction of the image. To solve that a more refined algorithm is needed - but that is second to you have a real way to access pixels on an image - like using Python's Imaging Library (PIL or Pillow), or pygame, or some other library.

Categories

Resources