The Ravel kit #

For use with a simulator (more information here):
- Wokwi simulation;
- configuration: .
Run button to (re)launch the application.
Headless (without GUI) #
Reset #
Resetting the kit
import ucuq
lcd = ucuq.Ravel.LCD()
ring = ucuq.Ravel.Ring()
buzzer = ucuq.Ravel.Buzzer()
oled = ucuq.Ravel.OLED()
Buzzer #
Beep #
import ucuq
FREQ = 440
buzzer = ucuq.Ravel.Buzzer()
buzzer.on(FREQ)
ucuq.sleep(1)
buzzer.off()
Scales #
import ucuq
FREQ_LOW = 220
FREQ_HIGH = 880
DELAY_TIME = .1
NUM_STEPS = 24
buzzer = ucuq.Ravel.Buzzer()
coeff = (FREQ_HIGH/FREQ_LOW)**(1/NUM_STEPS)
for i in range(NUM_STEPS):
buzzer.on(int(FREQ_LOW * coeff ** i))
ucuq.sleep(DELAY_TIME)
for i in range(NUM_STEPS + 1):
buzzer.on(int(FREQ_HIGH / coeff ** i))
ucuq.sleep(DELAY_TIME)
buzzer.off()
LCD #
Simple display #
import ucuq
MESSAGE = "Greetings!"
lcd = ucuq.Ravel.LCD()
lcd.putString(MESSAGE)
lcd.backlightOn()
Waves #
import ucuq
DELAY = 0.07
COUNT = 2
lcd = ucuq.Ravel.LCD()
lcd.uploadJaugeChars().backlightOn()
jauges = ()
def mirror(str):
return str[:8] + tuple(str[i] for i in range(7, -1, -1,))
def morph(a, b):
steps = max(abs(x - y) for x, y in zip(a, b))
for s in range(1, steps):
ucuq.sleep(DELAY)
lcd.putJauges(0, [int(ai + s / (steps - 1) * (bi - ai)) for ai, bi in zip(a, b)])
for j in range(0, 32 * COUNT):
jauges = ((abs((j + 16) % 32 - 16),) + jauges)[:16]
ucuq.sleep(DELAY)
lcd.putJauges(0, jauges)
morph(jauges, mirror(jauges))
for j in range(0, 32 * COUNT):
jauges = ((abs((j + 16) % 32 - 16),) + jauges)[:16]
ucuq.sleep(DELAY)
lcd.putJauges(0, mirror(jauges))
morph(mirror(jauges), (0,) * 16)
ucuq.sleep(DELAY)
lcd.backlightOff()
RGB Ring #
Plain #
import ucuq
R = 10
G = 8
B = 1
ring = ucuq.Ravel.Ring()
ring.fill((R, G, B)).write()
Rainbow #
import ucuq, random
MIN = 1
MAX = 5
ring = ucuq.Ravel.Ring()
for _ in range(15):
for led in range(8):
ring.setValue(led, tuple((lambda: random.randint(MIN, MAX))() for _ in range(3))).write()
ucuq.sleep(.03)
ucuq.sleep(2)
ring.fill((0, 0, 0)).write()
OLED #
Drawing #
import ucuq
oled = ucuq.Ravel.OLED()
oled.draw("03c00c30181820044c32524a80018001824181814812442223c410080c3003c0", 16, mul=4, ox=32).show()
Multi-components #
Pink panther #
import ucuq, random, zlib, base64
RING_MAX = 10
RING_MIN = 2
buzzer = ucuq.Ravel.Buzzer()
oled = ucuq.Ravel.OLED()
lcd = ucuq.Ravel.LCD()
ring = ucuq.Ravel.Ring()
unpack = lambda data: zlib.decompress(base64.b64decode(data)).decode()
def rainbowGradient(n, max):
colors = []
for i in range(n):
h = (i * 6) / n
x = int((1 - abs((h % 2) - 1)) * max)
if 0 <= h < 1: r, g, b = max, x, 0
elif 1 <= h < 2: r, g, b = x, max, 0
elif 2 <= h < 3: r, g, b = 0, max, x
elif 3 <= h < 4: r, g, b = 0, x, max
elif 4 <= h < 5: r, g, b = x, 0, max
else: r, g, b = max, 0, x
colors.append((r, g, b))
return colors
RAINBOW = rainbowGradient(50, RING_MAX)
def voiceCallback(freq):
global led
if freq !=-1:
buzzer.off()
if freq != 0:
buzzer.on(freq)
ring.setValue(led % 8, RAINBOW[led % len(RAINBOW)]).write()
led += 1
ring.setValue(led % 8,(0,0,0)).write()
lcd.displayRing(ring, RING_MAX)
def durationCallback(duration, cumul):
global pantherPict, pantherDelay
if cumul > pantherDelay * pantherPict:
oled.fill(0).show()
oled.draw(unpack(PANTHER[pantherPict % len(PANTHER)]), 128).show()
pantherPict += 1
ucuq.sleepWait(duration)
ucuq.sleepStart()
def main():
lcd.backlightOn().uploadJaugeChars()
ucuq.sleepStart()
ucuq.playVoices(VOICES, 120, lambda freq: voiceCallback(freq), lambda duration, cumul: durationCallback(duration, cumul))
lcd.clear()
TEXT = " " * 14 + "That's all folks!" + " " * 16
for i in range(64):
ring.setValue(((led + (i // 8)) % 8),(0,0,0)).write()
ucuq.sleepStart()
oled.scroll(0, 1).show()
lcd.moveTo(0,0).putString(TEXT[i//2:i//2+16])
ucuq.sleepWait(0.07)
oled.fill(0).show()
lcd.backlightOff()
ring.fill((0, 0, 0)).write()
PANTHER = (
'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'
)
VOICES = ("""
D#43, E44 R4, F#43, G44 R4, D#43, E43,-2, R2, F#43, G43,-2, R2, C53, B43,-2, R2, E43, G43,-2, R2, B43, Bb45-3, A43, G43, E43, D43, E43,-4 R5-3,
D#43, E44 R4, F#43, G44 R4, D#43, E43,-2, R2, F#43, G43,-2, R2, C53, B43,-2, R2, E43, G43,-2, R2, E53, Eb56.. R4,
D#43, E44 R4, F#43, G44 R4, D#43, E43,-2, R2, F#43, G43,-2, R2, C53, B43,-2, R2, E43, G43,-2, R2, B43, Bb45-3, A43, G43, E43, D43, E43,-4 R5-4,
R3... Eb51 E54, D53, B44, A43, G44, E43, Bb42 A43. Bb42 A43. Bb42 A43. Bb42 A43. G43, E43, D43, E44, E43,-5 R6
F#44, G43,-3 R3-5 A44, Bb43-4-3 C#53, D54, F55 -4, Eb53, Db54, Bb44, -4.
R4. G43, Bb42 C52 Bb42 G42 A44, G43, -4 R4, Bb43, Db52 Eb52 Db52 Bb42 C54, Bb44, -4, G44, F43,-6.-4, F#44, G43
R5 G43, Bb44, C53, D54, D53, Db53, C53, Bb43, G44, Bb43, R4, G43, Bb44, C54, Db54, Db54, C54, Bb44, G45. F44, D43, -4 R5.
G42 Bb42 R3 Bb44 F#42 A42 R3 A44 G44
""",)
pantherDelay = 60 // len(PANTHER)
pantherPict = 0
led = random.randrange(len(RAINBOW))
LED_LIMITER = 15
main()
With GUI #
Boilerplate #
A minimalist application to switch on/off the 7-segment display (simulation) or the embedded LED (physical kit).
import ucuq, atlastk
led = ucuq.GPIO(8)
async def atk(dom):
await dom.inner("", BODY)
led.high()
async def atkSwitch(dom, id):
if await dom.getValue(id) == "true":
led.low()
else:
led.high()
BODY = """
<fieldset>
<label>
<span>On/off: </span>
<input type="checkbox" xdh:onevent="Switch">
</label>
</fieldset>
"""
atlastk.launch(globals=globals())
Chernoff faces #
Application displaying Chernov faces.
import atlastk, math, ucuq
lcd = ucuq.Ravel.LCD()
oled = ucuq.Ravel.OLED()
class ChernoffFace:
def __init__(self, fb, width, height, cx, cy):
self.fb = fb
self.width = width
self.height = height
self.cx = cx
self.cy = cy
self.face_color = 1
# 18 paramètres du visage de Chernoff
self.face_width = 1.0 # 1. Largeur du visage (ratio)
self.face_height = 1.0 # 2. Hauteur du visage (ratio)
self.face_shape = 1.0 # 3. Forme du visage (-1 à 1, rond vs ovale)
self.eye_size = 0.5 # 4. Taille des yeux (0 à 1)
self.eye_spacing = 0.5 # 5. Écartement des yeux (0 à 1)
self.eye_height = 0.0 # 6. Position verticale des yeux (-1 à 1)
self.eye_eccentricity = 0.5 # 7. Excentricité des yeux (0=rond, 1=ovale)
self.eye_angle = 0.0 # 8. Inclinaison des yeux (-45 à 45 degrés)
self.pupil_size = 0.5 # 9. Taille des pupilles (0 à 1)
self.pupil_position = 0.0 # 10. Position horizontale pupilles (-1 à 1)
self.eyebrow_length = 0.5 # 11. Longueur des sourcils (0 à 1)
self.eyebrow_angle = 0.0 # 12. Inclinaison sourcils (-45 à 45 degrés)
self.eyebrow_height = 0.2 # 13. Position verticale sourcils (0 à 1)
self.nose_length = 0.5 # 14. Longueur du nez (0 à 1)
self.nose_width = 0.5 # 15. Largeur du nez (0 à 1)
self.mouth_width = 0.5 # 16. Largeur de la bouche (0 à 1)
self.mouth_height = 0.0 # 17. Position verticale bouche (-1 à 1)
self.mouth_curve = 0.0 # 18. Courbure de la bouche (-1=triste, 1=souriant)
# --- Méthodes de configuration individuelles ---
def set_face_dimensions(self, width_ratio, height_ratio, shape):
self.face_width = max(0.5, min(1.5, width_ratio))
self.face_height = max(0.5, min(1.5, height_ratio))
self.face_shape = max(-1, min(1, shape))
def set_eyes(self, size, spacing, height, eccentricity, angle):
self.eye_size = max(0, min(1, size))
self.eye_spacing = max(0, min(1, spacing))
self.eye_height = max(-1, min(1, height))
self.eye_eccentricity = max(0, min(1, eccentricity))
self.eye_angle = max(-45, min(45, angle))
def set_pupils(self, size, position):
self.pupil_size = max(0, min(1, size))
self.pupil_position = max(-1, min(1, position))
def set_eyebrows(self, length, angle, height):
self.eyebrow_length = max(0, min(1, length))
self.eyebrow_angle = max(-45, min(45, angle))
self.eyebrow_height = max(0, min(1, height))
def set_nose(self, length, width):
self.nose_length = max(0, min(1, length))
self.nose_width = max(0, min(1, width))
def set_mouth(self, width, height, curve):
self.mouth_width = max(0, min(1, width))
self.mouth_height = max(-1, min(1, height))
self.mouth_curve = max(-1, min(1, curve))
def set_from_dict(self, params):
"""Configure tous les paramètres depuis un dictionnaire"""
for key, value in params.items():
if hasattr(self, key):
setattr(self, key, value)
# --- Méthode de dessin ---
def draw(self):
self._draw_face_outline()
self._draw_eyebrows()
self._draw_eyes()
self._draw_pupils()
self._draw_nose()
self._draw_mouth()
# --- Détails du dessin ---
def _draw_face_outline(self):
rx = int(self.width * self.face_width / 2)
ry = int(self.height * self.face_height / 2)
# Ajustement de forme (ellipse vs cercle)
if self.face_shape > 0:
ry = int(ry * (1 + self.face_shape * 0.3))
else:
rx = int(rx * (1 - self.face_shape * 0.3))
self.fb.ellipse(self.cx, self.cy, rx, ry, self.face_color)
def _draw_eyes(self):
spacing = int(self.width * 0.3 * self.eye_spacing)
y_offset = int(self.height * 0.15 * self.eye_height)
eye_y = self.cy - self.height // 6 + y_offset
rx = int(self.width * 0.08 * self.eye_size)
ry = int(rx * (1 - self.eye_eccentricity * 0.5))
angle = math.radians(self.eye_angle)
for side in (-1, 1):
ex = self.cx + side * spacing
self._ellipse_rotated(ex, eye_y, rx, ry, angle, self.face_color)
def _draw_pupils(self):
if self.pupil_size == 0:
return
spacing = int(self.width * 0.3 * self.eye_spacing)
y_offset = int(self.height * 0.15 * self.eye_height)
eye_y = self.cy - self.height // 6 + y_offset
pupil_r = int(self.width * 0.03 * self.pupil_size)
pupil_x_offset = int(self.width * 0.02 * self.pupil_position)
for side in (-1, 1):
px = self.cx + side * spacing + pupil_x_offset
self._fill_circle(px, eye_y, pupil_r, self.face_color)
def _draw_eyebrows(self):
if self.eyebrow_length == 0:
return
spacing = int(self.width * 0.3 * self.eye_spacing)
y_offset = int(self.height * 0.15 * self.eye_height)
base_y = self.cy - self.height // 6 + y_offset
brow_y = base_y - int(self.height * 0.1 * self.eyebrow_height) - 5
length = int(self.width * 0.1 * self.eyebrow_length)
angle = math.radians(self.eyebrow_angle)
for side in (-1, 1):
bx = self.cx + side * spacing
dx = int(length * math.cos(angle)) * side
dy = int(length * math.sin(angle)) * side
self.fb.line(bx - dx, brow_y - dy, bx + dx, brow_y + dy, self.face_color)
def _draw_nose(self):
if self.nose_length == 0:
return
nx = self.cx
nose_h = int(self.height * 0.15 * self.nose_length)
nose_w = int(self.width * 0.08 * self.nose_width)
ny_top = self.cy - nose_h // 2
ny_bottom = self.cy + nose_h // 2
half_w = nose_w // 2
x1, y1 = nx - half_w, ny_bottom
x2, y2 = nx + half_w, ny_bottom
x3, y3 = nx, ny_top
self.fb.line(x1, y1, x2, y2, self.face_color)
self.fb.line(x2, y2, x3, y3, self.face_color)
self.fb.line(x3, y3, x1, y1, self.face_color)
def _draw_mouth(self):
mw = int(self.width * 0.4 * self.mouth_width)
if mw < 2:
return
y_offset = int(self.height * 0.15 * self.mouth_height)
y_base = self.cy + self.height // 4 + y_offset
curve_amp = int(self.height * 0.1 * self.mouth_curve)
x1 = self.cx - mw // 2
steps = mw
lastx, lasty = x1, y_base
for i in range(1, steps + 1):
x = x1 + i
t = math.pi * i / steps
y = y_base - int(curve_amp * math.sin(t))
self.fb.line(lastx, lasty, x, y, self.face_color)
lastx, lasty = x, y
# --- Utilitaires de dessin ---
def _ellipse_rotated(self, x0, y0, rx, ry, angle, color):
steps = 20
for i in range(steps):
theta1 = 2 * math.pi * i / steps
theta2 = 2 * math.pi * (i + 1) / steps
x1 = rx * math.cos(theta1)
y1 = ry * math.sin(theta1)
x2 = rx * math.cos(theta2)
y2 = ry * math.sin(theta2)
px1 = x0 + x1 * math.cos(angle) - y1 * math.sin(angle)
py1 = y0 + x1 * math.sin(angle) + y1 * math.cos(angle)
px2 = x0 + x2 * math.cos(angle) - y2 * math.sin(angle)
py2 = y0 + x2 * math.sin(angle) + y2 * math.cos(angle)
self.fb.line(int(px1), int(py1), int(px2), int(py2), color)
def _fill_circle(self, x, y, r, color):
for dy in range(-r, r + 1):
dx = int(math.sqrt(r * r - dy * dy))
self.fb.hline(x - dx, y + dy, 2 * dx + 1, color)
chernoff = ChernoffFace(oled, 60, 60, 64, 32)
async def atk(dom):
await dom.inner("", BODY)
chernoff.draw()
oled.show()
async def atkSet(dom, id):
value = await dom.getValue(id)
lcd.clear().moveTo(0,0).putString(f"{id}:").moveTo(0,1).putString(f"{value}").backlightOn()
exec(f"chernoff.{id} = {float(value)}")
oled.fill(0)
chernoff.draw()
oled.show()
BODY = """
<fieldset style="display: flex;">
<legend>Chernoff's faces (AI-generated)</legend>
<span>
<fieldset style="display:flex; flex-direction: column">
<legend>Eyes</legend>
<input type="range" id="eye_size" min="0" max="1" step="0.01" value="0.5" xdh:onevent="Set">
<input type="range" id="eye_spacing" min="0" max="1" step="0.01" value="0.5" xdh:onevent="Set">
<input type="range" id="eye_height" min="-1" max="1" step="0.01" value="0" xdh:onevent="Set">
<input type="range" id="eye_eccentricity" min="0" max="1" step="0.01" value="0.5" xdh:onevent="Set">
<input type="range" id="eye_angle" min="-45" max="45" step="1" value="0" xdh:onevent="Set">
</fieldset>
<fieldset style="display:flex; flex-direction: column">
<legend>Pupil</legend>
<input type="range" id="pupil_size" min="0" max="1" step="0.01" value="0.5" xdh:onevent="Set">
<input type="range" id="pupil_position" min="-1" max="1" step="0.01" value="0" xdh:onevent="Set">
</fieldset>
<fieldset style="display:flex; flex-direction: column">
<legend>Nose</legend>
<input type="range" id="nose_length" min="0" max="1" step="0.01" value="0.5" xdh:onevent="Set">
<input type="range" id="nose_width" min="0" max="1" step="0.01" value="0.5" xdh:onevent="Set">
</fieldset>
</span>
<span>
<fieldset style="display:flex; flex-direction: column">
<legend>Face</legend>
<input type="range" id="face_width" min="0" max="1" step="0.01" value="1" xdh:onevent="Set">
<input type="range" id="face_height" min="0" max="1" step="0.01" value="1" xdh:onevent="Set">
<input type="range" id="face_shape" min="-1" max="1" step="0.01" value="1" xdh:onevent="Set">
</fieldset>
<fieldset style="display:flex; flex-direction: column">
<legend>Eyebrow</legend>
<input type="range" id="eyebrow_length" min="0" max="1" step="0.01" value="0.5" xdh:onevent="Set">
<input type="range" id="eyebrow_angle" min="-15" max="45" step="1" value="0" xdh:onevent="Set">
<input type="range" id="eyebrow_height" min="0" max="1" step="0.01" value="0.2" xdh:onevent="Set">
</fieldset>
<fieldset style="display:flex; flex-direction: column">
<legend>Mouth</legend>
<input type="range" id="mouth_width" min="0" max="1" step="0.01" value="0.5" xdh:onevent="Set">
<input type="range" id="mouth_height" min="-1" max="1" step="0.01" value="0" xdh:onevent="Set">
<input type="range" id="mouth_curve" min="-1" max="1" step="0.01" value="0" xdh:onevent="Set">
</fieldset>
</span>
</fieldset>"""
atlastk.launch(globals=globals())
Simon’s game #
A game inspired by Simon.
NOTA: for use with the simulator, uncomment (remove # and following space) the expression SIMULATOR = True at line 3.
import atlastk, ucuq, json, math, random
# SIMULATOR = True
seq = ""
userSeq = ""
lcd = ucuq.Ravel.LCD()
ring = ucuq.Ravel.Ring()
buzzer = ucuq.Ravel.Buzzer()
oled = ucuq.Ravel.OLED()
lcd.backlightOn()
trueBuzzer = buzzer
ringCount = 8
ringOffset = 2
ringLimiter = 255 if 'SIMULATOR' in globals() and SIMULATOR else 30
def buzzerBeep_(note, delay=0.29, sleep=0.01):
buzzer.play(57 + note)
ucuq.sleep(delay)
buzzer.off()
if sleep:
ucuq.sleep(sleep)
def ringFlash_(button):
ring.fill([0, 0, 0])
if button in BUTTONS:
for i in range(ringCount // 4):
ring.setValue(
(
list(BUTTONS.keys()).index(button) * ringCount // 4
+ i
+ ringOffset
)
% ringCount,
[ringLimiter * item // 255 for item in BUTTONS[button][0]],
)
ring.write()
def oledDigit_(n, offset):
oled.draw(DIGITS[n], 8, offset, mul=8)
def playJingle_(jingle):
prevButton = ""
prevPrevButton = ""
for n in jingle:
while True:
button = random.choice(list(BUTTONS.keys()))
if (button != prevButton) and (button != prevPrevButton):
break
prevPrevButton = prevButton
ringFlash_(prevButton := button)
buzzerBeep_(n, 0.15, 0)
ringFlash_("")
def oledNumber(n):
try:
oledDigit_(n // 10, 12)
oledDigit_(n % 10, 76)
except:
oled.fill(0)
oled.show()
def lcdClear():
lcd.clear()
def lcdDisplay(line, text):
lcd.moveTo(0, line).putString(text.ljust(16))
def lcdBacklightOn():
lcd.backlightOn()
def lcdBacklightOff():
lcd.backlightOff()
def lcdDisplaySequence(seq, l10n):
lcdClear()
lcdDisplay(0, "".join(l10n(12)["RGBY".index(char)] for char in seq))
lcdBacklightOn()
def begin(l10n):
lcdClear()
lcdDisplay(0, l10n(10).format(**l10n(new=8)))
lcdDisplay(1, l10n(11))
oledNumber(None)
commit()
return not isinstance(buzzer, ucuq.Nothing)
def new(seq, l10n):
oledNumber(None)
lcdClear()
lcdDisplay(0, l10n(3))
lcdDisplay(1, l10n(4))
oledNumber(0)
ucuq.sleep(0.75)
play(seq)
commit()
def restartHW(seq, l10n):
oledNumber(None)
lcdClear()
lcdDisplay(0, l10n(1))
lcdDisplay(1, l10n(2))
lcdBacklightOn()
playJingle_(LAUNCH_JINGLE)
ucuq.sleep(0.5)
new(seq, l10n)
def success(l10n):
lcdDisplay(0, l10n(5))
oledNumber(None)
oled.draw(HAPPY_MOTIF, 16, mul=4, ox=32).show()
playJingle_(SUCCESS_JINGLE)
ucuq.sleep(0.5)
lcdClear()
commit()
def failure(l10n):
lcdDisplay(0, l10n(6).format(**l10n(new=8)))
lcdDisplay(1, l10n(7))
oledNumber(len(seq))
buzzer.on(30)
oledNumber(None)
oled.fill(0).draw(SAD_MOTIF, 16, mul=4, ox=32).show()
ucuq.sleep(1)
buzzer.off()
commit()
def buzzerSwitch(status):
global buzzer
if status:
buzzer = trueBuzzer
else:
buzzer = ucuq.Nothing()
def displayButton(button):
ringFlash_(button)
buzzer.play(57 + BUTTONS[button][2])
ucuq.sleep(0.29)
buzzer.off()
ring.fill([0, 0, 0]).write()
ucuq.sleep(0.01)
def play(sequence):
seq = ""
for s in sequence:
oledNumber(len(seq) + 1)
displayButton(s)
seq += s
if len(seq) % 5:
commit()
def commit():
ucuq.commit()
def getValuesOfVarsBeginningWith(prefix):
return [value for var, value in globals().items() if var.startswith(prefix)]
def remove(source, items):
return [item for item in source if item not in items]
BUTTONS = {
"R": [[255, 0, 0], 5, 9],
"B": [[0, 0, 255], 7, 12],
"Y": [[255, 255, 0], 1, 17],
"G": [[0, 255, 0], 3, 5],
}
LAUNCH_JINGLE = [3, 10, 0, 8, 7, 5, 3, 7, 10, 8, 5, 3]
SUCCESS_JINGLE = [7, 10, 19, 15, 17, 22]
async def restartAwait(dom):
global seq
seq = random.choice("RGBY")
restartHW(seq, lambda m: dom.getL10n(m))
await dom.setValue("Length", str(len(seq)))
async def atk(dom):
global seq, userSeq
body = BODY.format(**dom.getL10n(new=8, repeat=9))
ucuq.setCommitBehavior(ucuq.CB_MANUAL)
await dom.inner("", body)
seq = ""
userSeq = ""
if begin(lambda *args, **kwargs: dom.getL10n(*args, **kwargs)):
await dom.setAttribute("Buzzer", "checked", "")
async def atkRepeat():
play(seq)
commit()
async def atkNew(dom):
await restartAwait(dom)
async def atkClick(dom, id):
global seq, userSeq
if not seq:
return
userSeq += id
lcdDisplaySequence(userSeq, lambda m: dom.getL10n(m))
oledNumber(len(seq) - len(userSeq))
displayButton(id)
if seq.startswith(userSeq):
if len(userSeq) >= len(seq):
success(lambda m: dom.getL10n(m))
userSeq = ""
seq += random.choice("RGBY")
new(seq, lambda m: dom.getL10n(m))
ucuq.sleep(1.5)
await dom.setValue("Length", str(len(seq)))
else:
lcdBacklightOff()
else:
failure(lambda *args, **kwargs: dom.getL10n(*args, **kwargs))
userSeq = ""
seq = ""
commit()
async def atkSwitchSound(dom, id):
buzzerSwitch(await dom.getValue(id) == "true")
ATK_HEAD = """
<style>
#outer-circle {
background: #385a94;
border-radius: 50%;
height: 360px;
width: 360px;
position: relative;
border-style: solid;
border-width: 5px;
margin: auto;
box-shadow: 8px 8px 15px 5px #888888;
}
#G {
position: absolute;
height: 180px;
width: 180px;
border-radius: 180px 0 0 0;
-moz-border-radius: 180px 0 0 0;
-webkit-border-radius: 180px 0 0 0;
background: darkgreen;
top: 50%;
left: 50%;
margin: -180px 0px 0px -180px;
border-style: solid;
border-width: 5px;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
#R {
position: absolute;
height: 180px;
width: 180px;
border-radius: 0 180px 0 0;
-moz-border-radius: 0 180px 0 0;
-webkit-border-radius: 0 180px 0 0;
background: darkred;
top: 50%;
left: 50%;
margin: -180px 0px 0px 0px;
border-style: solid;
border-width: 5px;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
#Y {
position: absolute;
height: 180px;
width: 180px;
border-radius: 0 0 0 180px;
-moz-border-radius: 0 0 0 180px;
-webkit-border-radius: 0 0 0 180px;
background: goldenrod;
top: 50%;
left: 50%;
margin: 0px -180px 0px -180px;
border-style: solid;
border-width: 5px;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
#B {
position: absolute;
height: 180px;
width: 180px;
border-radius: 0 0 180px 0;
-moz-border-radius: 0 0 180px 0;
-webkit-border-radius: 0 0 180px 0;
background: darkblue;
top: 50%;
left: 50%;
margin: 0px 0px -180px 0px;
border-style: solid;
border-width: 5px;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
#inner-circle {
position: absolute;
background: grey;
border-radius: 50%;
height: 180px;
width: 180px;
border-style: solid;
border-width: 10px;
top: 50%;
left: 50%;
margin: -90px 0px 0px -90px;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
button {
font-size: x-large;
margin-top: 5px;
}
</style>
"""
BODY = """
<fieldset>
<legend xdh:onevent="dblclick|UCUqXDevice">Simon's game</legend>
<input id="Color" type="hidden">
<div id="outer-circle">
<div id="G" xdh:onevent="Click"></div>
<div id="R" xdh:onevent="Click"></div>
<div id="Y" xdh:onevent="Click"></div>
<div id="B" xdh:onevent="Click"></div>
<div id="inner-circle" style="display: flex;justify-content: center;align-items: center; flex-direction: column;">
<label style="display: flex; background-color: white; margin-top: 1px;">
<span style="font-size: large;">🎵:</span>
<input id="Buzzer" type="checkbox" xdh:onevent="SwitchSound">
</label>
<div>
<button xdh:onevent="New">{new}</button>
</div>
<div>
<button xdh:onevent="Repeat">{repeat}</button>
</div>
<output id="Length" style="background-color: white; display: inline-grid; width: 2rem; margin-top:5px; justify-content: center;"> </output>
</div>
</div>
</fieldset>
"""
ATK_L10N = (
(
"en",
"fr",
"de"
),
(
"Watch, Remember,",
"Observer, memo-",
"Achten, merken,"
),
(
"Repeat!",
"riser, repeter !",
"wiederholen! "
),
(
"Reproduce the",
"Reproduire la",
"Sequenz"
),
(
"sequence...",
"sequence...",
"wiederholen..."
),
(
"Well done!",
"Bravo!",
"Gut gemacht!"
),
(
"Game over! '{new}'",
"Perdu! '{new}'",
"Verloren! '{new}'"
),
(
"to play again!",
"pour rejouer!",
"zum Neustart!"
),
(
"New",
"Nouveau",
"Neu"
),
(
"Repeat",
"Répéter",
"Wiederhol."
),
(
"Click '{new}' to",
"'{new}'",
"'{new}' anklicken",
),
(
"begin the game!",
"pour commencer !",
"um zu beginnen!"
),
(
"RGBY",
"RVBJ",
"RGBY",
),
)
DIGITS = (
"708898A8C88870",
"20602020202070",
"708808304080f8",
"f8081030088870",
"10305090f81010",
"f880f008088870",
"708880f0888870",
"f8081020404040",
"70888870888870",
"70888878088870",
)
HAPPY_MOTIF = "03c00c30181820044c32524a80018001824181814812442223c410080c3003c0"
SAD_MOTIF = "03c00c30181820044c3280018001824181814002400227e410080c3003c0"
OLED_COEFF = 8
atlastk.launch(globals=globals())
Rock-Paper-Scissors #
import atlastk, ucuq, random, math
lcd = ucuq.Ravel.LCD()
ring = ucuq.Ravel.Ring(2)
buzzer = ucuq.Ravel.Buzzer()
oled = ucuq.Ravel.OLED()
class LED_:
def __init__(self, position):
self.position_ = position
def setValue(self, col):
ring.setValue(self.position_, col)
def groupLeds_(leds, positions):
for position in positions:
leds.add(LED_(position))
RING_MAX_ = 10
COLOR_1_ = (0, RING_MAX_, 0)
COLOR_2_ = (0, 0, RING_MAX_)
COLOR_TIE_ = (RING_MAX_, RING_MAX_, 0)
ROCK_ = 0
PAPER_ = 1
SCISSORS_ = 2
NOTHING_ = 3
noMaster_ = True
ledsStatus_ = ucuq.Multi()
ledsPlayer1_ = ucuq.Multi()
ledsPlayer2_ = ucuq.Multi()
ledsTie_ = ucuq.Multi()
def setAllLedGroups_():
groupLeds_(ledsStatus_, (0, 7))
groupLeds_(ledsPlayer1_, (4, 5))
groupLeds_(ledsTie_, (3, 4))
groupLeds_(ledsPlayer2_, (2, 3))
def play_(song):
ucuq.sleepStart()
ucuq.playVoices((song,), 120,
lambda freq: (
buzzer.off(),
buzzer.on(freq) if freq != 0 else None
) if freq != -1 else None,
lambda duration: (
ucuq.sleepWait(duration),
ucuq.sleepStart()
)
)
def lcdScoreAndWinner_(score1, score2, winner, L10n):
score = score1 if winner == 1 else score2
scoreString = str(score - 1).center(8) + str(score).center(8)
line2 = " " * 16 + (L10n(6) if winner == 0 else L10n(5).format(winner)).center(16)
for i in range(17):
if winner != 0:
lcd.moveTo(0 if winner == 1 else 8, 0).putString(scoreString[i // 2: i // 2 + 8])
lcd.moveTo(0, 1).putString(line2[i : i + 16])
# ucuq.sleep(0.00001 * (2 if winner else 1) )
def newGame_(dom):
global player1_, score1_, score2_
player1_ = None
score1_, score2_ = 0, 0
ring.fill((0, 0, 0))
ledsStatus_.setValue(COLOR_TIE_)
ring.write()
play_(INTRO_SONG_)
lcd.moveTo(0,0)\
.putString("0".center(8) * 2)\
.moveTo(0,1)\
.putString(dom.getL10n(4).center(16))\
.backlightOn()
async def atkbDisplay(dom, extra):
def getClass(winner, player):
if winner == 3:
return "tie"
elif winner == player:
return "winner"
else:
return "pending"
winner = int(extra[:1])
if not str(id(dom)) == extra[3:] or winner != 0:
await dom.removeClasses({"Player1": ALL_CLASSES_, "Player2": ALL_CLASSES_})
await dom.addClasses(
{
"Player1": [CLASSES_[int(extra[1])], getClass(winner, 1)],
"Player2": [CLASSES_[int(extra[2])], getClass(winner, 2)],
}
)
await dom.setValues(
{
"Status": dom.getL10n(1).format(1 if player1_ is None else 2),
"Score1": score1_,
"Score2": score2_,
}
)
async def atk(dom):
global noMaster_
if noMaster_:
noMaster_ = False
dom.isMaster = True
newGame_(dom)
await dom.inner("", BODY_)
await atkbDisplay(dom, f"0{NOTHING_}{NOTHING_}{id(dom)}")
await dom.setValues({"Title": dom.getL10n(2), "New": dom.getL10n(3), "Score": dom.getL10n(7)})
async def atkNew(dom):
newGame_(dom)
atlastk.broadcastAction(atkbDisplay, f"0{NOTHING_}{NOTHING_}dummyObjectId")
async def handleMove_(dom, move):
global player1_, score1_, score2_
ring.fill((0, 0, 0))
toAll = f"0{NOTHING_}{NOTHING_}{id(dom)}"
if player1_ is None:
player1_ = move
oled.fill(0).draw(OLED_PICTURES_[3], 128).show()
lcd.moveTo(0,1).putString(dom.getL10n(1).format(2).replace("…", "...").center(16))
song = MOVE_SONG_
await dom.removeClasses({"Player1": ALL_CLASSES_, "Player2": ALL_CLASSES_})
await dom.addClasses(
{
"Player1": [CLASSES_[move], "pending"],
"Player2": [CLASSES_[NOTHING_], "pending"],
}
)
else:
toAll = f"{player1_}{move}{id(dom)}"
match (winner := WINNER_[(player1_, move)]):
case 0:
ledsTie_.setValue(COLOR_TIE_)
song = TIE_SONG_
toAll = "3" + toAll
case 1:
score1_ += 1
ledsPlayer1_.setValue(COLOR_1_)
song = SONG_1_
toAll = "1" + toAll
case 2:
score2_ += 1
ledsPlayer2_.setValue(COLOR_2_)
song = SONG_2_
toAll = "2" + toAll
oled.draw(FLIPPED_OLED_PICTURES_[player1_], 64).draw(
OLED_PICTURES_[move], 64, ox=64
)
if winner != 0:
oled.rect(0 if winner == 1 else 64, 0, 64, 64, 1)
oled.show()
player1_ = None
if score1_ < score2_:
ledsStatus_.setValue(COLOR_2_)
elif score1_ == score2_:
ledsStatus_.setValue(COLOR_TIE_)
else:
ledsStatus_.setValue(COLOR_1_)
ring.write()
atlastk.broadcastAction(atkbDisplay, toAll)
play_(song)
if player1_ is None:
lcdScoreAndWinner_(
score1_,
score2_,
winner,
lambda i : dom.getL10n(i)
)
async def atkClick(dom, id):
await handleMove_(dom, int(id))
async def atkAIMove(dom):
await handleMove_(dom, random.randrange(3))
def flip_(hexString: str, width: int) -> str:
bits = "".join(f"{int(c, 16):04b}" for c in hexString)
flippedBitsLines = []
for y in range(len(bits) // width):
line = bits[y * width : (y + 1) * width]
flippedLines = line[::-1]
flippedBitsLines.append(flippedLines)
flippedBits = "".join(flippedBitsLines)
result = ""
for i in range(len(flippedBits) // 4):
nibble = flippedBits[4 * i : 4 * (i + 1)]
result += f"{int(nibble, 2):x}"
return result
ATK_L10N = (
("en", "fr", "de"),
("To player {}…", "Au joueur {}…", "An Spieler {} …"),
("Rock paper scissors", "Pierre-feuille-ciseaux", "Schere, Stein, Papier"),
("New game", "Nouvelle partie", "Neues Spiel"),
("Let's play!", "C'est parti!", "Los geht's!"),
("Player {} wins!", "Joueur {} gagne !", "Spieler {} siegt!"),
("It's a tie!", "Match nul !", "Unentschieden!"),
("Score", "Score", "Stand")
)
WINNER_ = {
(ROCK_, ROCK_): 0,
(ROCK_, PAPER_): 2,
(ROCK_, SCISSORS_): 1,
(PAPER_, ROCK_): 1,
(PAPER_, PAPER_): 0,
(PAPER_, SCISSORS_): 2,
(SCISSORS_, ROCK_): 2,
(SCISSORS_, PAPER_): 1,
(SCISSORS_, SCISSORS_): 0,
}
INTRO_SONG_ = "C42 C52 B42 D42 E42 A42 G42"
TIE_SONG_ = "C24"
SONG_1_ = "C42 E42 G42"
SONG_2_ = "C42 G42 E42"
MOVE_SONG_ = "G41"
OLED_PICTURES_ = (
"""000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e000000000000003f000000000000007f80000000000000ffc0000000000000ffe00000000000007f000000000000007e00000000000000787000000000007e31fe0000000000ff03ff8000000000ff87ffe000000000ff1ffff000000000fc3ffff800000000f8fffffc0000000071fffffe0000000003fe7fff0000000f07fe3fff0000001f8ffc1fff8000003fc7f80fff8000003fe7e00fff8000003fe1c30fff8000001ff8071fffc000000ffe1e1fffc0000007ff801fffc0000001ffc09fffc0000000ffc71fffc000007c7fcf1fffc000007e1fcf1fffc00000ff0fcf9fffc00000ffc79f8fffe00000ffe03fcffff00000fff07fcffffc00003ff8ffe7fffe00001ffcffe3ffff000007fcfff1ffff000003fdfff8ffff000000f9fffdfffe00000071fffffffe00000003fffffffc00000007fffffffc0000007ffffffff80000007ffffffff00000003ffffffff00000000fffffffe000000003ffffffc000000001ffffff80000000001fffff80000000000007fe00000000000007fc00000000000003f800000000000001f000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000""",
"""000000000000000000000000000000000000000000000000000000000000000000000000000000000000038000000000000007c000000000000007e000000000001e07f000000000001f03f800000000003f83fc00000000001fc1fe00000000000fe0fe000000000007f07f000000000007f87f800600000003fc3fc00f000001e1fe1fe01f800001f0ff0fe01f800003f87f87f01fc00001fc3fc3f81fe00000fe1fe3fc1fe00000ff0ff1fc0ff000007f87f8ff0ff800003fc7f87f87f800001fe3fc7fc3f800000ff1ffffe3fc000307f8fffff3fc000383fc7fffe3fe0003c1fe3fffe3fe0003e0ff1fcfe7fe0001f07f9fcfe7fe0001fc3fff8fe7ff0000fe1fff9fcfff00007f0fff9fcffe00003f87ff1fcffe00003fc7ff3fcffe00001fe1fe3fcffe00000ff1fe7fcffe000007fffcffeffe000003fff8fffffe000001fff1fffffe0000007ff3fffffe0000003ffffffffe0000001ffffffffe00000007fffffffe00000007ffffffff00000003ffffffff00000001ffffffff00000001ffffffff80000000ffffffff800000003fffffff800000001fffffff0000000007ffffff0000000003fffffe0000000000fffffc00000000007ffff800000000000ffff0000000000001ffe00000000000000fc00000000000000f00000000000000000000000000000000000000000000000000000000000000000000""",
"""00000000000000000000000000000000000000000000000000000e000000000000000f000000000000001f800000000000000fc00000000000000fc000000000000007e000000000000007e000000000000007f000000000000003f800000000000001fc00000000000001fc00000000078001fe000000000ff000fe000000000ff800ff0000000007fe007f0000000003ff807fc000000001ffc07fc000000000ffe03fe0000000003ffc3ff0000000000ffe10000000000007ff83000000000000ff87e000000000007f1ffe00000000001e3fff80000000000cfffff00000000001fffffc0000000003ffbffe0000000007fe1fff0000000003f80fff000000000001c7ff800000000003e3ff800000007e0ff8ff800000007f81fc7fc00000007ff0fe7fc00000003ffc1e7fc00000003fff0e7fe00000000fffc67fe000000003ffc67fe000000000ffe63fe0000000001fe63fe00000003e03c73ff00000003fc00f3ff00000003ff01f9ff80000003ffc7f8ffc0000000ffe3fcfff00000007ff3fffff00000000ff3fffff000000003f3fffff00000000007ffffe0000000000fffffc000000000ffffffc0000000007fffff80000000003fffff00000000000ffffe000000000003fffc00000000000007f000000000000003e00000000000000180000000000000000000000000000000000000000000000000000""",
"""00000000000001c0000000000000000000000000000003e0000000000000000000000000000007f00000001fe00000000000000000000ff0000003ffff0000000000000000001ff000000fffffe000000000000000003ff000003ffffff800000000000000007ff00000fffffffe0000000000000000ffe00001ffffffff0000000000000001ffc00003ffffffff8000000000000003ff800007ffffffffc000000000000003ff80000fffffffffe000000000000007ff00001ffffffffff00000000000000ffe00001ffffffffff00000000003801ffc00003ffffffffff800000000fffe3ff800003ffffc7ffff800000007fffc7ff000007fffe00ffffc0000003ffff8ffe000007fff8003fffc000000fffff1ffc000007fff0003fffc000003fffff3ff8000007ffe0001fffc000007ffffe3ff800000fffe0000fffe00001fffffc7ff180000fffc0000fffe00003fffff8ffe3c0000fffc0000fffe00007fffff1ffc7e0000fffc0000fffe0000fffffe3ff8ff0000fffc0000fffc0001ffefcc7ff1ff8000fffc0000fffc0003ff8f80ffe1ffc00000000001fffc0007ff0f01ffc0ffe00000000001fffc000ffc0f01ffc03ff00000000003fff8001ff80f03ff881ff80000000003fff8001ff01f07ff180ff80000000007fff0003fe01f0ffe3807fc000000001ffff0007fc00f1ffc7003fe000000003fffe0007f800e3ff8f001fe000000007fffc000ff800c7ff1f001ff00000000ffff80007f8008ffe3f001fe00000003ffff00007fc001ffc7e003fe00000007fffc00003fe001ffcfe007fc0000000ffff800001ff003ff8fc00ff80000000fffe000001ff807ff1fc01ff80000001fffc000000ffe0ffe3f807ff00000001fff80000007ff1ffc7f00ffe00000003fff00000003fe3ff8fe01ffc00000003ffe00000001fc7ff1f807ff800000003ffc00000000f8ffe1c01fff000000003ffc0000000078ffc000fffe000000003ff80000000031ffc41ffffc000000003ff80000000003ff8ffffff8000000003ff80000000007ff1fffffe0000000000000000000000ffe3fffffc0000000000000000000001ffc7fffff00000000000000000000003ff8fffffc00000000000000000000007ff1ffffe00000000000000000000000ffe0ffff00000000000007c000000000ffe000000000000000001ff000000001ffc000000000000000003ff800000003ff8000000000000000007ff800000007ff0000000000000000007ffc0000000ffe0000000000000000007ffc0000000ffc0000000000000000007ff80000000ff80000000000000000003ff80000000ff00000000000000000001ff00000000ff000000000000000000007c000000007e0000000000000000000000000000003800000000000000000000000000000"""
)
CLASSES_ = {
ROCK_: "rock",
PAPER_: "paper",
SCISSORS_: "scissors",
NOTHING_: "nothing"
}
ALL_CLASSES_ = list(claz for claz in CLASSES_.values()) + ["tie", "winner", "pending"]
FLIPPED_OLED_PICTURES_ = tuple(flip_(picture, 64) for picture in OLED_PICTURES_)
ATK_HEAD = """
<style>
.hand {
background-repeat: no-repeat;
background-size: 100% 100%;
display: inline-block;
width: 94px;
height: 94px;
border: transparent 5px solid;
}
.sensitive {
cursor: pointer;
}
.rock {
background-image: url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDADUlKC8oITUvKy88OTU/UIVXUElJUKN1e2GFwarLyL6qurfV8P//1eL/5re6////////////zv//////////////2wBDATk8PFBGUJ1XV53/3Lrc////////////////////////////////////////////////////////////////////wAARCABeAF4DASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAIDBAUB/8QAKhAAAgIBAwMEAQQDAAAAAAAAAQIAAxEEEiExQVETImGBMiMzobE0cZH/xAAYAQADAQEAAAAAAAAAAAAAAAAAAQIDBP/EAB4RAQEBAQACAwEBAAAAAAAAAAABEQIhMQMSMkFR/9oADAMBAAIRAxEAPwDDERAEREASTI6jLKQPkTVoUU7nPJHA+JrYAqd2Md8zLr5MuKnOxyIlt6Ij/puGU+D0lU0l1JERGCIiAIiIB6il3CjqTidBdLUFwVyfJmRKrlxYEOBzNR1QYAVKWdu3iZd230vnP6rIGjtyMlG7dxJhLNQc2ZSvsvcyddGG32ne/wDAljuqKWY4Amd6/wA9niB01JGNgma3RleazuHjvNqkMoIOQZ7FO7DslceJu1dAZTYo9w6/Mwzo56+01nZhERKIllG31l3/AI57yuTqqa1sKP8AZ8RX0I6sz2oarRci5HRgP7kVNmmwH99fkdppVldQynIM5vy19qTq6tuQST4xKrEsvRnf2KBlVlmqrHpmxRh15BE8Wl7UU22kqRnaOJUyTYV30hoA3uOTt6Y+ZsmfRfsfZmiT3+qfPonJtXZay+DOtObq/wDJf6/qX8V8l2piIm7Mm3QugQrnDE/9mKJPXP2mHLjsHkYMzNS1TF6PtPMqo1ZX228jzNVl6JXu3A+Md5hnXNxeyqN1mrUALsrzyc9ZqAAAA6CVaZClIDdTzLour5yHGek+le9R6MdyzRK76vVXg4YcgySbtg343d8QvnyIlM2p03qEun5dx5mmIpbLsOzXHIIOCMERLdS4suJHQcSqdU8xiRERgm7TLQ4DKvuHUE9JhnqsVOVODJ6mw5cdeJhr1rAYdc/Ilo1tXhh9TC8dRp9o0xKE1dbuFGRnuZfJss9nuko1V3p17Qfc38Sd1y0rk8nsJzXdrHLMeTL443zU9XEYiJ0MyIiAIiIAiIgCaV1jCrbjLeTM0RWS+zlx6zF23MckzyIjIiIgH//Z);
}
.paper {
background-image: url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDADUlKC8oITUvKy88OTU/UIVXUElJUKN1e2GFwarLyL6qurfV8P//1eL/5re6////////////zv//////////////2wBDATk8PFBGUJ1XV53/3Lrc////////////////////////////////////////////////////////////////////wAARCABeAF4DASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAIDBAUB/8QAKhAAAgIBAwMEAQQDAAAAAAAAAQIAAxEEITESQVETIjJhcnGBobEzNEL/xAAYAQADAQEAAAAAAAAAAAAAAAAAAQIDBP/EACARAAICAgICAwAAAAAAAAAAAAABAhEhMQMSIjJRYXH/2gAMAwEAAhEDEQA/AMMRAGSAO8AETadCvRsx6vviZ6x6WoX1BjB3kqSeh1RBkdPkpGfIkZ12VXUhhkGcy+o0vg8djJhPsNxoriImhIiIgAiIgAklrdgWVSQO4kZ0tKytQoXtsRInLqrGlZ5prxauD8xz9z3UUC5dtmHBlWooKt6tOxG5Al1FwuXww5ExePKJf0ynS3FT6NmxHGf6mi2pbUKt+x8SvU0eoOpdnH8xprvUXpbZ15+4PPkgXwzBYjVuVbkSIGTgczpaigXJtsw4Mw0+zUL17YO+ZtGdolqmWto2Wot1ZYb4madic/V0+m/Uo9rfwZMJ26Y5RrRniImpAl1Zt0zBypCnt5lI2M6iMmoq3GQeR4kTdFRVkkdbFDKcgzPdSyN6tOxHI8yBSzSP1L7qzzNaOtihlOQZj65Wit7I02rcmRz3HiQtoLWCyshXHP3DUEXCyo9Jz7h5l8V07Q97Ez6nTer7l2f+5KuxnvsH/C7fvLLG6K2bwMwVxYbRVpLC9ZVvkm0suT1KmXzxK9IvTQD3bcy+EsSwC0cfiJZqBi9wPMrnSsoyEtouNL55U8iVRBq8AdZWWxMjBUzM6tpX603rPI8TPRe1LeVPInRR1sXKnImDTh+GidnqkMoI4IzIXWCqst37frLJQamsv6rPgvxHn7kKryNktNWa6hn5NuZ5qv8AWfEuggEYIyIXmwrFEKtqU/EScREM5V5ze/5GQk7v81n5H+5XOtaMWexERgJOm1qnDDjuPMhEGrA6yMHUMpyDJTl03tSdt17idCq5LRlTv3Hec0oOJqnZZERIGIiIAc7WV9FxPZt5nmnWXCxwq8L3madUbrJk9nsREoQiIgAgEqcg4P1E8gBqr1rqMOOr74MuXW1k4IYTnxIfHFldmdjIxnIx5mPU6oEFKz+rTKXYqFLEgcDMjJjxpZYOQiImpJ//2Q==);
}
.scissors {
background-image: url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDADUlKC8oITUvKy88OTU/UIVXUElJUKN1e2GFwarLyL6qurfV8P//1eL/5re6////////////zv//////////////2wBDATk8PFBGUJ1XV53/3Lrc////////////////////////////////////////////////////////////////////wAARCABeAF4DASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAEDBAUC/8QAKRAAAgIBAwQCAgEFAAAAAAAAAQIAAxEEITESE0FRInEygaEzQmFisf/EABgBAAMBAQAAAAAAAAAAAAAAAAACAwEE/8QAHREAAgICAwEAAAAAAAAAAAAAAAECESExAxJBIv/aAAwDAQACEQMRAD8AowhCABJKKTc+M4A5Mjk2ks6LgDw20yV1g1bFqKDSRvlTwZDNa2sWVlT5mWylGKtyIkJdlk2SoUIRSgo4RRwAIQhABohscKvJlltCwHxcE+iMSvW5rcMORNGnUJbsDhvRk5uS0NFJma6MjYYEGKaz1rYuHGRKN2kZN0+S/wAiEeRPYONFyizuVK3ng/ch1tPUvcUbjn6kWhs6XKHhuPuXjjgyT+JDLKMeEsamjtNlfwP8SvOhO1aJtUEIQmgEcUIAOEIoAWqdYybWfIe/MupYti5Q5EyJ3WQHGWKg8kScuNPKGUi3cEe0Clc2g5JHA+52NKrDNrFnPJzJKu0iYrK4+5zZqUXZPm3gLJ29Ial6cNpD0lVtYA+DuJVs01te5XI9iXabuslHHTYORJod5R2FJmNCaOp06ujMow4328zOloyUkI1QQhCMYOKEIAE1EppNYwikY5xMuT6ey5P6all8jGREmm1gaLJ7NEp3rPSfR4glnY2spC/7KJ2mqRjhwUb0ZPsw8EGRbepDUvCJ0r1ChlbccMPEKLHZnR8Ep5HmcdAp1KCvYPnKx1/HWWqf7gCIeAWJl6hO3cy+ORNSUteu6P8AqbxumElgpwhCdBMcUcIAKX9JcnbCEhWHvzKMUWUeyo1OjXetLBh1BlW3TNWC9LsAORmV6r7K8ANt6PEuF73TArUZH5dW0lUose0zqitQBYGLlh+TRXo3UttYyy+PYklSdutUznE7iXmzawE4trW1Olv0fU7iJABJOAIqNMq2tqn6W/R9zmSX2920t44EinWrrJFjhCE0AijigASanUPVwcr6MhjmNXsDQTWVMPllT/mSJdW7dKsCZlwBIIIOCJN8S8G7M12YKCWOAJQ1OpNvxXZP+yGy17Dl2JnM2PHWWDlYo4oSgp//2Q==);
}
.nothing {
background-image: url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDADUlKC8oITUvKy88OTU/UIVXUElJUKN1e2GFwarLyL6qurfV8P//1eL/5re6////////////zv//////////////2wBDATk8PFBGUJ1XV53/3Lrc////////////////////////////////////////////////////////////////////wAARCABeAF4DASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAT/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AIQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/2Q==);
}
.winner {
border-color: red;
}
.tie {
border-color: transparent;
}
.pending {
border-color: transparent;
}
</style>
"""
BODY_ = """
<fieldset>
<legend id="Title"></legend>
<fieldset>
<div style="white-space: nowrap;">
<span id="0" xdh:onevent="Click" class="hand sensitive rock"></span>
<span id="1" xdh:onevent="Click" class="hand sensitive paper"></span>
<span id="2" xdh:onevent="Click" class="hand sensitive scissors"></span>
</div>
<br />
<div style="display: flex;">
<h2 style="margin:auto;" id="Status"></h2>
</div>
<br />
<div style="display: flex;">
<button xdh:onevent="AIMove" style="margin:auto; width: 5em; font-size: larger;">🤖</button>
</div>
</fieldset>
<fieldset>
<div style="width: 100%;">
<div style="display: flex; justify-content: space-around;">
<span id="Player1" style="transform: scaleX(-1)" class="hand nothing pending"></span>
<span id="Player2" class="hand nothing pending"></span>
</div>
<br />
<fieldset style="display: flex; justify-content: space-around;">
<span id="Score1"></span>
<span id="Score"></Span>
<span id="Score2"></span>
</fieldset>
</fieldset>
<br>
<div style="display: flex;">
<button xdh:onevent="New" style="margin: auto;" id="New"></button>
</div>
</fieldset>
"""
setAllLedGroups_()
atlastk.launch(globals=globals())
Morpion #
Tic-tac-toe game.
import atlastk, ucuq, random, math
lcd = ucuq.Ravel.LCD()
ring = ucuq.Ravel.Ring()
buzzer = ucuq.Ravel.Buzzer()
oled = ucuq.Ravel.OLED()
RING_MAX = 5
CROSS_COLOR = (0, RING_MAX, 0)
CIRCLE_COLOR = (0, 0, RING_MAX)
TIE_COLOR = (RING_MAX, RING_MAX, 0)
SOUND_DELAY = 0.5
CROSS_SOUND = "C42"
CIRCLE_SOUND = "F42"
TIE_SOUND = "F42 E42 D42 C43"
WIN_SOUND = "C42 D42 E42 F43"
INTRO_SOUND = "F43 G42 A42 Bb42 C52 D52 E52 F53"
W_AI_MOVE = "AIMove"
W_CLEAR = "Clear"
currentPlayer = ""
winningCombos = []
board = None
def soundCallback(freq):
if freq != -1:
buzzer.off()
if freq != 0:
buzzer.on(freq)
if currentPlayer == "":
for led in range(8):
ring.setValue(
led,
tuple(
int(math.exp(random.uniform(0, math.log(RING_MAX))))
for _ in range(3)
),
)
ring.write()
def soundPlay(sound):
ucuq.sleepStart()
ucuq.playVoices((sound,), 120, lambda freq: soundCallback(freq), lambda duration: (ucuq.sleepWait(duration), ucuq.sleepStart()))
def oledGrid():
oled.fill(0).rect(30, 0, 64, 64, 1).hline(31, 21, 63, 1).hline(31, 42, 63, 1).vline(51, 0, 63, 1).vline(72, 0, 63, 1).show()
def oledConvert(i, j):
return (41 + 21 * int(j), 11 + 21 * int(i))
def oledCircle(i, j):
x, y = oledConvert(i, j)
oled.ellipse(x, y, 6, 6, 1, False).show()
def oledCross(i, j):
x, y = oledConvert(i, j)
oled.line(x - 5, y - 5, x + 5, y + 5, 1).line(x - 5, y + 5, x + 5, y - 5, 1).show()
def oledWin(combo):
def sign(a, b):
return (a < b) - (a > b)
x1, y1 = oledConvert(*combo[0])
x2, y2 = oledConvert(*combo[2])
ox = 9 * sign(x1, x2)
oy = 9 * sign(y1, y2)
oled.line(x1 - ox, y1 - oy, x2 + ox, y2 + oy, 1).show()
def initWinningCombos():
global winningCombos
for i in range(3):
winningCombos.append(tuple((i, j) for j in range(3)))
winningCombos.append(tuple((j, i) for j in range(3)))
winningCombos.append(tuple((i, i) for i in range(3)))
winningCombos.append(tuple((i, 2 - i) for i in range(3)))
async def setStatus(dom, text):
if dom.isMaster:
text = " " * 16 + text.replace("…", "...") + " "
for _ in range(16):
text = text[1:]
lcd.moveTo(0, 1).putString(text[:16])
ucuq.sleep(0.01)
await dom.setValue("Status", text)
def newGame():
global currentPlayer, board
currentPlayer = ""
lcd.moveTo(0, 1).putString(" " * 16)
board = [["" for _ in range(3)] for _ in range(3)]
oledGrid()
soundPlay(INTRO_SOUND)
ring.fill((0, 0, 0))
currentPlayer = "x"
def getWinner(board):
for combo in winningCombos:
values = [board[i][j] for i, j in combo]
if values[0] != "" and len(set(values)) == 1:
return (values[0], combo)
for i in range(3):
for j in range(3):
if board[i][j] == "":
return (None, None)
return ("", None)
async def atkbDisplay(dom):
global currentPlayer
crossCount = 0
circleCount = 0
removedClasses = {}
addedClasses = {}
for i in range(3):
for j in range(3):
await dom.setValue(f"cell{i}{j}", board[i][j])
match board[i][j]:
case "x":
if dom.isMaster:
oledCross(i, j)
ring.setValue(1 - crossCount, CROSS_COLOR)
crossCount += 1
removedClasses[f"cell{i}{j}"] = ["o", "win"]
addedClasses[f"cell{i}{j}"] = "x"
case "o":
if dom.isMaster:
oledCircle(i, j)
ring.setValue(2 + circleCount, CIRCLE_COLOR)
circleCount += 1
removedClasses[f"cell{i}{j}"] = ["x", "win"]
addedClasses[f"cell{i}{j}"] = "o"
case "":
removedClasses[f"cell{i}{j}"] = ["x", "o", "win"]
winner, combo = getWinner(board)
if winner is not None:
addedClasses[W_AI_MOVE] = "hidden"
removedClasses[W_CLEAR] = "hidden"
else:
removedClasses[W_AI_MOVE] = "hidden"
addedClasses[W_CLEAR] = "hidden"
await dom.removeClasses(removedClasses)
await dom.addClasses(addedClasses)
if combo:
await dom.addClasses(dict(zip([f"cell{i}{j}" for i, j in combo], ["win"] * 3)))
if dom.isMaster:
oledWin(combo)
ring.fill(CROSS_COLOR if winner == "x" else CIRCLE_COLOR).write()
soundPlay(WIN_SOUND)
await setStatus(dom, dom.getL10n(6).format(winner.lower()))
currentPlayer = ""
return
elif winner == "":
if dom.isMaster:
ring.fill(TIE_COLOR).write()
soundPlay(TIE_SOUND)
await setStatus(dom, dom.getL10n(5))
currentPlayer = ""
return
if dom.isMaster:
if crossCount + circleCount:
ring.write()
await setStatus(dom, dom.getL10n(4).format(currentPlayer.lower()))
async def atk(dom):
await dom.inner("", BODY)
await dom.setValues({"Title": dom.getL10n(1), "Clear": dom.getL10n(3)})
dom.isMaster = board is None
if dom.isMaster:
lcd.clear().backlightOn().moveTo(0, 0).putString(dom.getL10n(2))
newGame()
await atkbDisplay(dom)
async def atkNew(dom):
newGame()
atlastk.broadcastAction(atkbDisplay)
def move(moves):
global currentPlayer
if currentPlayer == "":
return
i, j = moves[random.randrange(len(moves))]
if board[i][j] == "":
soundPlay(CROSS_SOUND if currentPlayer == "x" else CIRCLE_SOUND)
board[i][j] = currentPlayer
currentPlayer = "o" if currentPlayer == "x" else "x"
atlastk.broadcastAction(atkbDisplay)
async def atkClick(dom, id):
i = int(id[4])
j = int(id[5])
move(((i, j),))
def minimax(board, player):
winner, _ = getWinner(board)
match winner:
case "x":
return -1
case "o":
return 1
case "":
return 0
if player == "o":
score = -2
for i in range(3):
for j in range(3):
if board[i][j] == "":
board[i][j] = "o"
score = max(score, minimax(board, "x"))
board[i][j] = ""
return score
else:
score = 2
for i in range(3):
for j in range(3):
if board[i][j] == "":
board[i][j] = "x"
score = min(score, minimax(board, "o"))
board[i][j] = ""
return score
def bestMoves(board, player):
bestScore = -2 if player == "o" else 2
for i in range(3):
for j in range(3):
if board[i][j] == "":
board[i][j] = player
score = minimax(board, "x" if player == "o" else "o")
board[i][j] = ""
if score == bestScore:
moves.append((i, j))
elif (player == "o") ^ (score < bestScore):
bestScore = score
moves = [(i, j)]
return moves
def noCircle(board):
xMove = (0, 0)
for i in range(3):
for j in range(3):
match board[i][j]:
case "o":
return None
case "x":
xMove = (i, j)
return xMove
async def atkAIMove(dom):
if currentPlayer == "":
return
xMove = noCircle(board)
if xMove is None:
move(bestMoves(board, currentPlayer))
elif currentPlayer == "x":
move(((random.randrange(3), random.randrange(3)),))
else:
angles = tuple((i, j) for i in (0, 2) for j in (0, 2))
if xMove == (1, 1):
move(angles)
elif xMove in angles:
move(((1, 1),))
else:
moves = ((0, 0), (0, 2), (2, 1), (1, 1))
if xMove == (1, 0):
moves = tuple((j, i) for i, j in moves)
elif xMove == (2, 1):
moves = tuple((abs(i - 2), j) for i, j in moves)
elif xMove == (1, 2):
moves = tuple((j, abs(i - 2)) for i, j in moves)
move(moves)
ATK_L10N = (
("en", "fr", "de"),
("Tic-tac-toe", "Morpion", "Dodelschach"),
("Tic-tac-toe game", " Jeu du morpion ", " Dodelschach "),
("Clear", "Effacer", "Löschen"),
("To player {}…", "Au joueur {}…", "An Spieler {}…"),
("It's a tie!", "Match nul !", "Unentschieden!"),
("Winner: {}!", "Vainqueur : {} !", "Sieger: {}!"),
)
initWinningCombos()
ATK_HEAD = """
<link href="https://fonts.googleapis.com/css?family=Indie+Flower" rel="stylesheet">
<style>
h1,
h2 {
font-family: 'Indie Flower', 'Comic Sans', cursive;
text-align: center;
margin: 10px;
}
#board {
font-family: 'Indie Flower', 'Comic Sans', cursive;
position: relative;
font-size: 120px;
margin: 1% auto;
border-collapse: collapse;
}
#board td {
border: 4px solid rgb(60, 60, 60);
width: 90px;
height: 90px;
vertical-align: middle;
text-align: center;
cursor: pointer;
}
#board td div {
width: 90px;
height: 90px;
line-height: 90px;
display: block;
overflow: hidden;
cursor: pointer;
position: relative;
font-size: 1.2em;
}
.x {
color: lightgreen;
cursor: default !important;
}
.o {
color: lightblue;
cursor: default !important;
}
.win {
background-color: yellow;
}
.hidden {
display: none;
}
</style>
"""
BODY = """
<h1 id="Title"></h1>
<fieldset>
<table id="board">
<tbody>
<tr>
<td>
<div id="cell00" class="cell" xdh:onevent="Click"></div>
</td>
<td>
<div id="cell01" class="cell" xdh:onevent="Click"></div>
</td>
<td>
<div id="cell02" class="cell" xdh:onevent="Click"></div>
</td>
</tr>
<tr>
<td>
<div id="cell10" class="cell" xdh:onevent="Click"></div>
</td>
<td>
<div id="cell11" class="cell" xdh:onevent="Click"></div>
</td>
<td>
<div id="cell12" class="cell" xdh:onevent="Click"></div>
</td>
</tr>
<tr>
<td>
<div id="cell20" class="cell" xdh:onevent="Click"></div>
</td>
<td>
<div id="cell21" class="cell" xdh:onevent="Click"></div>
</td>
<td>
<div id="cell22" class="cell" xdh:onevent="Click"></div>
</td>
</tr>
</tbody>
</table>
<h2 id="Status"></h2>
<div style="display: flex;">
<button xdh:onevent="AIMove" style="margin:auto; width: 5em; font-size: larger;" id="AIMove">🤖</button>
<button xdh:onevent="New" style="margin: auto;" class="hidden" id="Clear"></button>
</div>
</fieldset>
"""
atlastk.launch(globals=globals())