code.py
  1  # SPDX-FileCopyrightText: 2020 Jeff Epler for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  import random
  6  import time
  7  
  8  import board
  9  import displayio
 10  import framebufferio
 11  import rgbmatrix
 12  
 13  displayio.release_displays()
 14  
 15  matrix = rgbmatrix.RGBMatrix(
 16      width=64, height=32, bit_depth=3,
 17      rgb_pins=[board.D6, board.D5, board.D9, board.D11, board.D10, board.D12],
 18      addr_pins=[board.A5, board.A4, board.A3, board.A2],
 19      clock_pin=board.D13, latch_pin=board.D0, output_enable_pin=board.D1)
 20  display = framebufferio.FramebufferDisplay(matrix, auto_refresh=False)
 21  
 22  # This bitmap contains the emoji we're going to use. It is assumed
 23  # to contain 20 icons, each 20x24 pixels. This fits nicely on the 64x32
 24  # RGB matrix display.
 25  
 26  filename = "emoji.bmp"
 27  
 28  # CircuitPython 6 & 7 compatible
 29  bitmap_file = open(filename, 'rb')
 30  bitmap = displayio.OnDiskBitmap(bitmap_file)
 31  pixel_shader = getattr(bitmap, 'pixel_shader', displayio.ColorConverter())
 32  
 33  # # CircuitPython 7+ compatible
 34  # bitmap = displayio.OnDiskBitmap(filename)
 35  # pixel_shader = bitmap.pixel_shader
 36  
 37  # Each wheel can be in one of three states:
 38  STOPPED, RUNNING, BRAKING = range(3)
 39  
 40  # Return a duplicate of the input list in a random (shuffled) order.
 41  def shuffled(seq):
 42      return sorted(seq, key=lambda _: random.random())
 43  
 44  # The Wheel class manages the state of one wheel. "pos" is a position in
 45  # scaled integer coordinates, with one revolution being 7680 positions
 46  # and 1 pixel being 16 positions. The wheel also has a velocity (in positions
 47  # per tick) and a state (one of the above constants)
 48  class Wheel(displayio.TileGrid):
 49      def __init__(self):
 50          # Portions of up to 3 tiles are visible.
 51          super().__init__(bitmap=bitmap, pixel_shader=pixel_shader,
 52                           width=1, height=3, tile_width=20, tile_height=24)
 53          self.order = shuffled(range(20))
 54          self.state = STOPPED
 55          self.pos = 0
 56          self.vel = 0
 57          self.y = 0
 58          self.x = 0
 59          self.stop_time = time.monotonic_ns()
 60  
 61      def step(self):
 62          # Update each wheel for one time step
 63          if self.state == RUNNING:
 64              # Slowly lose speed when running, but go at least speed 64
 65              self.vel = max(self.vel * 9 // 10, 64)
 66              if time.monotonic_ns() > self.stop_time:
 67                  self.state = BRAKING
 68          elif self.state == BRAKING:
 69              # More quickly lose speed when braking, down to speed 7
 70              self.vel = max(self.vel * 85 // 100, 7)
 71  
 72          # Advance the wheel according to the velocity, and wrap it around
 73          # after 7680 positions
 74          self.pos = (self.pos + self.vel) % 7680
 75  
 76          # Compute the rounded Y coordinate
 77          yy = round(self.pos / 16)
 78          # Compute the offset of the tile (tiles are 24 pixels tall)
 79          yyy = yy % 24
 80          # Find out which tile is the top tile
 81          off = yy // 24
 82  
 83          # If we're braking and a tile is close to midscreen,
 84          # then stop and make sure that tile is exactly centered
 85          if self.state == BRAKING and self.vel == 7 and yyy < 4:
 86              self.pos = off * 24 * 16
 87              self.vel = 0
 88              self.state = STOPPED
 89  
 90          # Move the displayed tiles to the correct height and make sure the
 91          # correct tiles are displayed.
 92          self.y = yyy - 20
 93          for i in range(3):
 94              self[i] = self.order[(19 - i + off) % 20]
 95  
 96      # Set the wheel running again, using a slight bit of randomness.
 97      # The 'i' value makes sure the first wheel brakes first, the second
 98      # brakes second, and the third brakes third.
 99      def kick(self, i):
100          self.state = RUNNING
101          self.vel = random.randint(256, 320)
102          self.stop_time = time.monotonic_ns() + 3_000_000_000 + i * 350_000_000
103  
104  # Our fruit machine has 3 wheels, let's create them with a correct horizontal
105  # (x) offset and arbitrary vertical (y) offset.
106  g = displayio.Group()
107  wheels = []
108  for idx in range(3):
109      wheel = Wheel()
110      wheel.x = idx * 22
111      wheel.y = -20
112      g.append(wheel)
113      wheels.append(wheel)
114  display.show(g)
115  
116  # Make a unique order of the emoji on each wheel
117  orders = [shuffled(range(20)), shuffled(range(20)), shuffled(range(20))]
118  
119  # And put up some images to start with
120  for si, oi in zip(wheels, orders):
121      for idx in range(3):
122          si[idx] = oi[idx]
123  
124  # We want a way to check if all the wheels are stopped
125  def all_stopped():
126      return all(si.state == STOPPED for si in wheels)
127  
128  # To start with, though, they're all in motion
129  for idx, si in enumerate(wheels):
130      si.kick(idx)
131  
132  # Here's the main loop
133  while True:
134      # Refresh the display (doing this manually ensures the wheels move
135      # together, not at different times)
136      display.refresh(minimum_frames_per_second=0)
137      if all_stopped():
138          # Once everything comes to a stop, wait a little bit and then
139          # start everything over again.  Maybe you want to check if the
140          # combination is a "winner" and add a light show or something.
141          for idx in range(100):
142              display.refresh(minimum_frames_per_second=0)
143          for idx, si in enumerate(wheels):
144              si.kick(idx)
145  
146      # Otherwise, let the wheels keep spinning...
147      for idx, si in enumerate(wheels):
148          si.step()