Advertisement
cookertron

Spatial Hash Plinko Board - Python Pygame

May 24th, 2020
1,624
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.73 KB | None | 0 0
  1. # Created by Anthony Cook & Daniel Pope
  2. # fb.com/groups/pygame
  3.  
  4.  
  5. # NOTES:
  6. # Requires Python 2.0.0dev8
  7. # use: pip install pygame==2.0.0dev8
  8. # Use WSAD to move the viewport & UP/DOWN arrow keys to zoom in and out
  9.  
  10. import pygame
  11. from pygame import gfxdraw
  12. import random
  13. import os
  14. import time
  15. from itertools import product
  16.  
  17.  
  18. class SpatialHash:
  19.     def __init__(self):
  20.         self.grid = {}
  21.  
  22.     def insert_point(self, point):
  23.         x, y = point // 64
  24.         cell = (x, y)
  25.         items = self.grid.get(cell)
  26.         if items is None:
  27.             self.grid[cell] = [point]
  28.         else:
  29.             items.append(point)
  30.  
  31.     def insert(self, rect, value):
  32.         for cell in self._rect_cells(rect):
  33.             items = self.grid.get(cell)
  34.             if items is None:
  35.                 self.grid[cell] = [value]
  36.             else:
  37.                 items.append(value)
  38.  
  39.     def _rect_cells(self, rect):
  40.         x1, y1 = rect.topleft
  41.         x1 //= 64
  42.         y1 //= 64
  43.         x2, y2 = rect.bottomright
  44.         x2 = x2 // 64 + 1
  45.         y2 = y2 // 64 + 1
  46.         return product(range(x1, x2), range(y1, y2))
  47.  
  48.     def query(self, rect):
  49.         items = []
  50.         for cell in self._rect_cells(rect):
  51.             items.extend(self.grid.get(cell, ()))
  52.         return items
  53.  
  54.     def query_point(self, pos):
  55.         x, y = pos // 64
  56.         return self.grid.get((x, y), ())
  57.  
  58.  
  59. # define a marble class
  60. class marble:
  61.     def __init__(s, x, y):
  62.         # pos is a vector (x, y)
  63.         s.pos = pygame.math.Vector2(x, y)
  64.         # velocity is also a vector with a starting x velocity of random (-1, 1) * 10
  65.         s.velocity = pygame.math.Vector2(random.uniform(-1, 1), 0) * 10
  66.  
  67.     def update(s):
  68.         global GRAVITY, FPS, STAGE
  69.  
  70.         # apply velocity to the marble's position
  71.         s.pos += s.velocity
  72.  
  73.         # apply gravity to the marble's velocity
  74.         s.velocity += GRAVITY / FPS
  75.  
  76.         # if a marble goes outside of the stage then it reappears
  77.         # on the opposite side. For instance if the marble drops out of the
  78.         # bottom of the stage then it will reappear at the top
  79.         if s.pos.y >= STAGE.bottom:
  80.             s.pos.y = 0
  81.         if s.pos.x > STAGE.right:
  82.             s.pos.x = 0
  83.         if s.pos.x < 0:
  84.             s.pos.x = STAGE.right
  85.  
  86. # pygame display settings
  87. DR = pygame.Rect((0, 0, 1280, 720)) # Display Rectangle
  88. HDW, HDH = DR.center # H = half
  89. FPS = 60
  90.  
  91. # set up pygame
  92. pygame.init()
  93. PD = pygame.display.set_mode(DR.size) # primary display based of the size of Display Rect (800, 600)
  94. CLOCK = pygame.time.Clock()
  95.  
  96. # set strength of gravity
  97. GRAVITY = pygame.math.Vector2(0, 9.8)
  98.  
  99. # set up stage
  100. SCALE = 10
  101. STAGE = pygame.Rect((0, 0, DR.w * SCALE, DR.h * SCALE))
  102.  
  103. # Create a spatial hash for broad phase collision detection
  104. spatial = SpatialHash()
  105.  
  106. # add 1000 randomly placed Plinko pins to quadtree
  107. PIN_COUNT = 1000
  108. pins = []
  109. for index in range(PIN_COUNT):
  110.     pos = pygame.math.Vector2(
  111.         random.randint(0, STAGE.w),
  112.         random.randint(0, STAGE.h)
  113.     )
  114.     bounds = pygame.Rect(pos.x - 30, pos.y - 30, 60, 60)
  115.     spatial.insert(bounds, pos)
  116.     pins.append(pos)
  117.  
  118. # create 1000 marbles
  119. MARBLE_COUNT = 1000
  120. MARBLE_SPACING = STAGE.w / MARBLE_COUNT
  121. MARBLES = [marble(index * MARBLE_SPACING, 0) for index in range(MARBLE_COUNT)]
  122.  
  123. # the viewport is like a window that looks onto the stage
  124. # this sets the location and size of the viewport which has a minimum size set to
  125. # that of the primary display surface and starts in the top left of the stage
  126. VIEWPORT = pygame.Rect(DR)
  127.  
  128. # exit the demo?
  129. exit = False
  130.  
  131. fps = 0
  132. frames = 1
  133.  
  134. while True:
  135.     start = time.time()
  136.     # process events
  137.     for e in pygame.event.get():
  138.         if e.type == pygame.QUIT: # window close gadget
  139.             exit = True
  140.    
  141.     k = pygame.key.get_pressed()
  142.     if k[pygame.K_ESCAPE] or exit: break
  143.  
  144.     if k[pygame.K_DOWN]:
  145.         # increase the size of the viewport
  146.         VIEWPORT.w += 18
  147.         VIEWPORT.h += 10
  148.     elif k[pygame.K_UP]:
  149.         # decrease the size of the viewport
  150.         VIEWPORT.w -= 18
  151.         VIEWPORT.h -= 10
  152.     # limit the mimium size of the viewport
  153.     # to that of the primary display resolution
  154.     if VIEWPORT.w < DR.w:
  155.         VIEWPORT.w = DR.w
  156.         VIEWPORT.h = DR.h
  157.  
  158.     if k[pygame.K_a]:
  159.         VIEWPORT.x -= 10
  160.     elif k[pygame.K_d]:
  161.         VIEWPORT.x += 10
  162.     if k[pygame.K_w]:
  163.         VIEWPORT.y -= 10
  164.     elif k[pygame.K_s]:
  165.         VIEWPORT.y += 10
  166.     # limit the viewport to stage's boundry
  167.     if VIEWPORT.right > STAGE.w:
  168.         VIEWPORT.x = STAGE.w - VIEWPORT.w
  169.     if VIEWPORT.x < 0:
  170.         VIEWPORT.x = 0
  171.     if VIEWPORT.bottom > STAGE.h:
  172.         VIEWPORT.y = STAGE.h - VIEWPORT.h
  173.     if VIEWPORT.y < 0:
  174.         VIEWPORT.y = 0
  175.  
  176.     # clear the primary display (fill it with black)
  177.     PD.fill((0, 0, 0))
  178.  
  179.     # calculate the scale of the viewport against the size of the primary display
  180.     scale = VIEWPORT.w / DR.w
  181.  
  182.     # draw all of the Plinko board's pins if they're within the viewport
  183.     cull = VIEWPORT.inflate(20, 20)
  184.     radius = 20 / scale
  185.     if VIEWPORT.w < 200:
  186.         visible_pins = spatial.query(VIEWPORT)
  187.     else:
  188.         visible_pins = [p for p in pins if cull.collidepoint(p)]
  189.     for pin in visible_pins:
  190.         pos = (pin - VIEWPORT.topleft) / scale
  191.         pygame.draw.circle(PD, (255, 0, 0), pos, radius)
  192.  
  193.     # draw all of the marbles and update marble position
  194.     radius = 6 / scale
  195.     if radius < 1:
  196.         color = (0, round(radius * 255), 0)
  197.         radius = 1
  198.     else:
  199.         color = (0, 255, 0)
  200.     for m in MARBLES:
  201.         if cull.collidepoint(m.pos):
  202.             pos = (m.pos - VIEWPORT.topleft) / scale
  203.             pygame.draw.circle(PD, color, pos, radius)
  204.  
  205.         # update position of the marble (apply velocity)
  206.         m.update()
  207.  
  208.         possible_hits = spatial.query_point(m.pos)
  209.  
  210.         # if there are pins then determine if the marble has collided with them
  211.         for cp in possible_hits:
  212.             # is the distance between the marble and the pin less than
  213.             # their combined radius's?
  214.             if cp.distance_to(m.pos) <= 26:
  215.                 # if yes then a collision has occurred and we need to calculate a new
  216.                 # trajectory for the marble. This basically the opposite direction to which
  217.                 # it was going
  218.                 angle = pygame.math.Vector2().angle_to(cp - m.pos)
  219.                 m.velocity = m.pos.rotate(angle - 180).normalize() * 10
  220.  
  221.     # update the primary display
  222.     #end = time.time()
  223.     #frames += 1
  224.     #fps += end - start
  225.     #os.write(1, f"FPS: {1 / (fps / frames)}, {scale}\n".encode('ascii'))
  226.     pygame.display.update()
  227.     CLOCK.tick(FPS) # limit frames
  228.     start = time.time()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement