Generate random number outside of range in python

后端 未结 5 1262
盖世英雄少女心
盖世英雄少女心 2021-02-18 22:52

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

5条回答
  •  灰色年华
    2021-02-18 23:31

    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

提交回复
热议问题