game/poisson.py

110 lines
3.5 KiB
Python

# -*- coding: utf-8 -*-
#
# Copyright © 2024 Simon Forman
#
# This file is part of game
#
# game is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# game is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with game. If not see <http://www.gnu.org/licenses/>.
#
'''
Poisson Distribution
'''
from math import pi, sqrt, sin, cos, hypot
from random import random, randint
tau = 2 * pi
def poisson(w, h, d, test_points=30):
'''
Return a list of coordinates generated by a Poisson point process.
Return a list of two-tuples, pairs of ints, representing points in a 2D
plane. The points are bounded by the width and height (w and h) and
separated by a minimum distance of d.
'''
grid, x, y = Grid(d), randint(0, w), randint(0, h)
grid.add(x, y)
todo = [(x, y)]
while todo:
x, y = todo.pop(0)
todo.extend(
(nx, ny)
for nx, ny in candidates(x, y, d, test_points)
if 0 <= nx <= w and 0 <= ny <= h and grid.add(nx, ny)
)
return list(grid.grid.values())
class Grid(object):
'''
Keep track of points in a plane and efficiently check that added points
are not nearer than a certain distance d to any others.
'''
def __init__(self, d):
self.d = d
self.r = d / sqrt(2) # A square of side r and diagonal d.
self.grid = {} # Map from grid "bins" to points, 1-to-1.
def add(self, x, y):
'''
Add a point pair if it's not too close to any others and return a
bool: True if the point was added, False if it was rejected.
'''
grid_coords = round(x / self.r), round(y / self.r)
if any(hypot(x - nx, y - ny) < self.d
for nx, ny in self._neighborhood(grid_coords)):
return False
self.grid[grid_coords] = x, y
return True
def _neighborhood(self, coordinates):
'''Return a list of the points in the neighborhood of (x, y).'''
# Note, x and y here are grid coordinates, not plane coordinates.
# This method returns plane coordinates.
x, y = coordinates
return filter(None, map(self.grid.get, [
(x-1, y-2), (x, y-2), (x+1, y-2),
(x-2, y-1), (x-1, y-1), (x, y-1), (x+1, y-1), (x+2, y-1),
(x-2, y), (x-1, y), (x, y), (x+1, y), (x+2, y),
(x-2, y+1), (x-1, y+1), (x, y+1), (x+1, y+1), (x+2, y+1),
(x-1, y+2), (x, y+2), (x+1, y+2),
]))
def candidates(x, y, d, test_points):
'''Generate points at a distance from (x, y) from d to 2*d.'''
for _ in range(test_points):
radius = d + random() * d
angle = random() * tau
yield (
int(round(x + radius * cos(angle))),
int(round(y + radius * sin(angle))),
)
if __name__ == '__main__':
from random import seed
seed(23)
WIDTH, HEIGHT, DISTANCE = 80, 24, 4
P = set(poisson(WIDTH, HEIGHT, DISTANCE))
for y in range(HEIGHT + 1):
print(''.join(' *'[(x, y) in P] for x in range(WIDTH + 1)))