Skip to content

Commit e85939b

Browse files
authored
Merge pull request #1185 from kevinjwalters/rockpaperscissors
New Guide - rock, paper, scissors game for CLUE and CPB.
2 parents 92119d5 + fe80b0e commit e85939b

31 files changed

+3077
-0
lines changed

CLUE_Rock_Paper_Scissors/advanced/clue-multi-rpsgame.py

Lines changed: 567 additions & 0 deletions
Large diffs are not rendered by default.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# MIT License
2+
3+
# Copyright (c) 2020 Kevin J. Walters
4+
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
import struct
24+
25+
from adafruit_ble.advertising import Advertisement, LazyObjectField
26+
from adafruit_ble.advertising.standard import ManufacturerData, ManufacturerDataField
27+
28+
# These message should really include version numbers for the
29+
# the protocol and a descriptor for the encryption type
30+
31+
# From adafruit_ble.advertising
32+
# 0xFF is "Manufacturer Specific Data" as per list of types in
33+
# https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/
34+
MANUFACTURING_DATA_ADT = 0xFF
35+
ADAFRUIT_COMPANY_ID = 0x0822
36+
37+
# pylint: disable=line-too-long
38+
# From https://github.com/adafruit/Adafruit_CircuitPython_BLE_BroadcastNet/blob/c6328d5c7edf8a99ff719c3b1798cb4111bab397/adafruit_ble_broadcastnet.py#L84-L85
39+
ADAFRUIT_SEQ_ID = 0x0003
40+
41+
# According to https://github.com/adafruit/Adafruit_CircuitPython_BLE/blob/master/adafruit_ble/advertising/adafruit.py
42+
# 0xf000 (to 0xffff) is for range for Adafruit customers
43+
44+
# These four are used as part of prefix matching
45+
RPS_ENC_DATA_ID = 0xfe41
46+
RPS_KEY_DATA_ID = 0xfe42
47+
RPS_ROUND_ID = 0xfe43
48+
GM_JOIN_ID = 0xfe44
49+
50+
RPS_ACK_ID = 0xfe51
51+
52+
# Data formats for shared fields
53+
_DATA_FMT_ROUND = "B"
54+
_DATA_FMT_ACK = "B"
55+
_SEQ_FMT = "B"
56+
57+
58+
class RpsEncDataAdvertisement(Advertisement):
59+
"""An RPS (broadcast) message.
60+
This sends the encrypted choice of the player.
61+
This is not connectable and does not elicit a scan response
62+
based on defaults in Advertisement parent class.
63+
"""
64+
flags = None
65+
66+
_PREFIX_FMT = "<BHBH"
67+
_DATA_FMT_ENC_DATA = "8s"
68+
69+
# match_prefixes tuple replaces deprecated prefix
70+
# comma for 1 element is very important!
71+
match_prefixes = (
72+
struct.pack(
73+
_PREFIX_FMT,
74+
MANUFACTURING_DATA_ADT,
75+
ADAFRUIT_COMPANY_ID,
76+
struct.calcsize("<H" + _DATA_FMT_ENC_DATA),
77+
RPS_ENC_DATA_ID
78+
),
79+
)
80+
manufacturer_data = LazyObjectField(
81+
ManufacturerData,
82+
"manufacturer_data",
83+
advertising_data_type=MANUFACTURING_DATA_ADT,
84+
company_id=ADAFRUIT_COMPANY_ID,
85+
key_encoding="<H"
86+
)
87+
88+
enc_data = ManufacturerDataField(RPS_ENC_DATA_ID, "<" + _DATA_FMT_ENC_DATA)
89+
round_no = ManufacturerDataField(RPS_ROUND_ID, "<" + _DATA_FMT_ROUND)
90+
sequence_number = ManufacturerDataField(ADAFRUIT_SEQ_ID, "<" + _SEQ_FMT)
91+
"""Sequence number of the data. Used in acknowledgements."""
92+
ack = ManufacturerDataField(RPS_ACK_ID, "<" + _DATA_FMT_ACK)
93+
"""Round number starting at 1."""
94+
95+
def __init__(self, *, enc_data=None, round_no=None, sequence_number=None, ack=None):
96+
"""enc_data must be either set here in the constructor or set first."""
97+
super().__init__()
98+
if enc_data is not None:
99+
self.enc_data = enc_data
100+
if round_no is not None:
101+
self.round_no = round_no
102+
if sequence_number is not None:
103+
self.sequence_number = sequence_number
104+
if ack is not None:
105+
self.ack = ack
106+
107+
108+
class RpsKeyDataAdvertisement(Advertisement):
109+
"""An RPS (broadcast) message.
110+
This sends the key to decrypt the previous encrypted choice of the player.
111+
This is not connectable and does not elicit a scan response
112+
based on defaults in Advertisement parent class.
113+
"""
114+
flags = None
115+
116+
_PREFIX_FMT = "<BHBH"
117+
_DATA_FMT_KEY_DATA = "8s"
118+
119+
# match_prefixes tuple replaces deprecated prefix
120+
# comma for 1 element is very important!
121+
match_prefixes = (
122+
struct.pack(
123+
_PREFIX_FMT,
124+
MANUFACTURING_DATA_ADT,
125+
ADAFRUIT_COMPANY_ID,
126+
struct.calcsize("<H" + _DATA_FMT_KEY_DATA),
127+
RPS_KEY_DATA_ID
128+
),
129+
)
130+
manufacturer_data = LazyObjectField(
131+
ManufacturerData,
132+
"manufacturer_data",
133+
advertising_data_type=MANUFACTURING_DATA_ADT,
134+
company_id=ADAFRUIT_COMPANY_ID,
135+
key_encoding="<H"
136+
)
137+
138+
key_data = ManufacturerDataField(RPS_KEY_DATA_ID, "<" + _DATA_FMT_KEY_DATA)
139+
round_no = ManufacturerDataField(RPS_ROUND_ID, "<" + _DATA_FMT_ROUND)
140+
sequence_number = ManufacturerDataField(ADAFRUIT_SEQ_ID, "<" + _SEQ_FMT)
141+
"""Sequence number of the data. Used in acknowledgements."""
142+
ack = ManufacturerDataField(RPS_ACK_ID, "<" + _DATA_FMT_ACK)
143+
"""Round number starting at 1."""
144+
145+
def __init__(self, *, key_data=None, round_no=None, sequence_number=None, ack=None):
146+
"""key_data must be either set here in the constructor or set first."""
147+
super().__init__()
148+
if key_data is not None:
149+
self.key_data = key_data
150+
if round_no is not None:
151+
self.round_no = round_no
152+
if sequence_number is not None:
153+
self.sequence_number = sequence_number
154+
if ack is not None:
155+
self.ack = ack
156+
157+
158+
class RpsRoundEndAdvertisement(Advertisement):
159+
"""An RPS (broadcast) message.
160+
This informs other players the round_no is complete.
161+
An important side-effect is acknowledgement of previous message.
162+
This is not connectable and does not elicit a scan response
163+
based on defaults in Advertisement parent class.
164+
"""
165+
flags = None
166+
167+
_PREFIX_FMT = "<BHBH"
168+
169+
# match_prefixes tuple replaces deprecated prefix
170+
# comma for 1 element is very important!
171+
match_prefixes = (
172+
struct.pack(
173+
_PREFIX_FMT,
174+
MANUFACTURING_DATA_ADT,
175+
ADAFRUIT_COMPANY_ID,
176+
struct.calcsize("<H" + _DATA_FMT_ROUND),
177+
RPS_ROUND_ID
178+
),
179+
)
180+
manufacturer_data = LazyObjectField(
181+
ManufacturerData,
182+
"manufacturer_data",
183+
advertising_data_type=MANUFACTURING_DATA_ADT,
184+
company_id=ADAFRUIT_COMPANY_ID,
185+
key_encoding="<H"
186+
)
187+
188+
round_no = ManufacturerDataField(RPS_ROUND_ID, "<" + _DATA_FMT_ROUND)
189+
sequence_number = ManufacturerDataField(ADAFRUIT_SEQ_ID, "<" + _SEQ_FMT)
190+
"""Sequence number of the data. Used in acknowledgements."""
191+
ack = ManufacturerDataField(RPS_ACK_ID, "<" + _DATA_FMT_ACK)
192+
"""Round number starting at 1."""
193+
194+
def __init__(self, *, round_no=None, sequence_number=None, ack=None):
195+
"""round_no must be either set here in the constructor or set first."""
196+
super().__init__()
197+
if round_no is not None:
198+
self.round_no = round_no
199+
if sequence_number is not None:
200+
self.sequence_number = sequence_number
201+
if ack is not None:
202+
self.ack = ack
203+
204+
205+
class JoinGameAdvertisement(Advertisement):
206+
"""A join game (broadcast) message used as the first message to work out who is playing.
207+
This is not connectable and does not elicit a scan response
208+
based on defaults in Advertisement parent class.
209+
"""
210+
flags = None
211+
212+
_PREFIX_FMT = "<BHBH"
213+
_DATA_FMT = "8s" # this NUL pads for 8s if necessary
214+
215+
# match_prefixes tuple replaces deprecated prefix
216+
# comma for 1 element is very important!
217+
match_prefixes = (
218+
struct.pack(
219+
_PREFIX_FMT,
220+
MANUFACTURING_DATA_ADT,
221+
ADAFRUIT_COMPANY_ID,
222+
struct.calcsize("<H" + _DATA_FMT),
223+
GM_JOIN_ID
224+
),
225+
)
226+
manufacturer_data = LazyObjectField(
227+
ManufacturerData,
228+
"manufacturer_data",
229+
advertising_data_type=MANUFACTURING_DATA_ADT,
230+
company_id=ADAFRUIT_COMPANY_ID,
231+
key_encoding="<H"
232+
)
233+
234+
game = ManufacturerDataField(GM_JOIN_ID, "<" + _DATA_FMT)
235+
"""The name of the game, limited to eight characters."""
236+
237+
def __init__(self, *, game=None):
238+
super().__init__()
239+
if game is not None:
240+
self.game = game
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# MIT License
2+
3+
# Copyright (c) 2020 Kevin J. Walters
4+
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
from audiocore import WaveFile
24+
25+
26+
class SampleJukeboxError(OSError):
27+
"""Exception raised for any missing audio files.
28+
"""
29+
30+
def __init__(self, files):
31+
self.files = files
32+
super().__init__("Missing audio files: " + ", ".join(files))
33+
34+
35+
class SampleJukebox():
36+
"""This plays wav files and tries to control the timing of memory
37+
allocations within the nRF52840 PWMAudioOut library to minimise
38+
the chance of MemoryError exceptions (2048 bytes)."""
39+
40+
_file_buf = None # Use for WaveFile objects
41+
42+
def _init_wave_files(self, files, directory):
43+
"""Open files from AUDIO_DIR and return a dict with FileIO objects
44+
or None if file not present."""
45+
46+
# 2048 triggers bug in https://github.com/adafruit/circuitpython/issues/3030
47+
self._file_buf = bytearray(512) # DO NOT CHANGE size til #3030 is fixed
48+
49+
missing = []
50+
fhs = {}
51+
for file in files:
52+
wav_file = None
53+
filename = directory + "/" + file + ".wav"
54+
try:
55+
wav_file = open(filename, "rb")
56+
fhs[file] = WaveFile(wav_file, self._file_buf)
57+
except OSError:
58+
# OSError: [Errno 2] No such file/directory: 'filename.ext'
59+
missing.append(filename)
60+
61+
# Raises an exception at the end to allow it to report ALL
62+
# of the missing files in one go to help out the user
63+
if missing:
64+
raise SampleJukeboxError(missing)
65+
self._wave_files = fhs
66+
67+
68+
def __init__(self, audio_device, files,
69+
directory="", error_output=None):
70+
self._audio_device = audio_device
71+
self._error_output = error_output
72+
self._wave_files = None # keep pylint happy
73+
self._init_wave_files(files, directory=directory)
74+
75+
# play a file that exists to get m_alloc called now
76+
# but immediately stop it with pause()
77+
for wave_file in self._wave_files.values():
78+
if wave_file is not None:
79+
self._audio_device.play(wave_file, loop=True)
80+
self._audio_device.pause()
81+
break
82+
83+
84+
def play(self, name, loop=False):
85+
wave_file = self._wave_files.get(name)
86+
if wave_file is None:
87+
return
88+
# This pairing of stop() and play() will cause an m_free
89+
# and immediate m_malloc() which reduces considerably the risk
90+
# of losing the 2048 contiguous bytes needed for this
91+
self._audio_device.stop()
92+
self._audio_device.play(wave_file, loop=loop)
93+
# https://github.com/adafruit/circuitpython/issues/2036
94+
# is a general ticket about efficient audio buffering
95+
96+
97+
def playing(self):
98+
return self._audio_device.playing
99+
100+
101+
def wait(self):
102+
while self._audio_device.playing:
103+
pass
104+
105+
106+
def stop(self):
107+
self._audio_device.pause() # This avoid m_free

0 commit comments

Comments
 (0)