The Ravel kit#

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 ucuq.ravel.raz()

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()

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()

LCD#

Simple display#

NAME = """ """.strip().rjust(16) import ucuq lcd = ucuq.ravel.LCD() lcd.putString("Greetings".ljust(16) + NAME).backlightOn()

Waves#

import ucuq DELAY = 0.07 COUNT = 2 lcd = ucuq.ravel.LCD() lcd.uploadGaugeChars().backlightOn() gauges = () 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.putGauges(0, [int(ai + s / (steps - 1) * (bi - ai)) for ai, bi in zip(a, b)]) for j in range(0, 32 * COUNT): gauges = ((abs((j + 16) % 32 - 16),) + gauges)[:16] ucuq.sleep(DELAY) lcd.putGauges(0, gauges) morph(gauges, mirror(gauges)) for j in range(0, 32 * COUNT): gauges = ((abs((j + 16) % 32 - 16),) + gauges)[:16] ucuq.sleep(DELAY) lcd.putGauges(0, mirror(gauges)) morph(mirror(gauges), (0,) * 16) ucuq.sleep(DELAY) lcd.backlightOff()

OLED#

Text#

import ucuq, datetime oled = ucuq.ravel.OLED() oled.text("We are the".center(16), 0, 20).text(f"{datetime.datetime.now().strftime("%d/%m/%Y %H:%M").center(16)}", 0, 40).show()

Drawing#

import ucuq oled = ucuq.ravel.OLED() oled.draw("03c00c30181820044c32524a80018001824181814812442223c410080c3003c0", 16, mul=4, ox=32).show()

Animation#

NOTE: It may take some seconds for the animation to start…

import base64 import zlib import ucuq oled = ucuq.ravel.OLED() ANIMATION = ( "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", ) for _ in range(10): for picture in ANIMATION: oled.draw(zlib.decompress(base64.b64decode(picture)).decode(), 128).show() oled.fill(0).show()

Servomotor#

import ucuq upper = ucuq.ravel.Upper() upper.set(ucuq.ravel.SERVO_MAX // 2) ucuq.sleep(0.5) upper.park()

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 ravel = ucuq.Ravel() lcd = ravel.lcd() oled = 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.

import atlastk, ucuq, json, math, random seq = "" userSeq = "" ravel = ucuq.Ravel() lcd = ravel.lcd() ring = ravel.ring() buzzer = ravel.buzzer() oled = ravel.oled() lcd.backlightOn() trueBuzzer = buzzer ringCount = 8 ringOffset = 2 ringLimiter = 31 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)).center(16)) lcdDisplay(1, l10n(11).center(16)) 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}' pour", "'{new}' anklicken", ), ( "begin the game!", "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 ravel = ucuq.Ravel(ringOffset=2) lcd = ravel.lcd() ring = ravel.ring() buzzer = ravel.buzzer() oled = 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 ravel = ucuq.Ravel() lcd = ravel.lcd() ring = ravel.ring() buzzer = ravel.buzzer() oled = 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())

Miscellaneous#

Multi-components#

import ucuq, random, zlib, base64 RING_MAX = 10 RING_MIN = 2 ravel = ucuq.Ravel() buzzer = ravel.buzzer() oled = ravel.oled() lcd = ravel.lcd() ring = 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() ravel.displayRingGauges() 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().uploadGaugeChars() 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()

Orientation#

This app works only on mobile devices (tablets, smartphones, etc.) equipped with the appropriate sensors and displays a 3D cube that represents the device’s orientation.

This app must be opened in its own tab to work (click on in the inset below)!

import atlastk import ucuq import math WIDTH_ = 128 HEIGHT_ = 64 buffer_ = [[0]*WIDTH_ for _ in range(HEIGHT_)] def setPixel_(x, y): if 0 <= x < WIDTH_ and 0 <= y < HEIGHT_: buffer_[y][x] = 1 def drawLine_(x0, y0, x1, y1): dx = abs(x1 - x0) dy = -abs(y1 - y0) sx = 1 if x0 < x1 else -1 sy = 1 if y0 < y1 else -1 err = dx + dy while True: setPixel_(x0, y0) if x0 == x1 and y0 == y1: break e2 = 2 * err if e2 >= dy: err += dy x0 += sx if e2 <= dx: err += dx y0 += sy def rotatePoint_(x, y, z, pitch, roll, yaw): p = math.radians(pitch) r = math.radians(roll) w = math.radians(yaw) # Pitch (X) y2 = y * math.cos(p) - z * math.sin(p) z2 = y * math.sin(p) + z * math.cos(p) # Yaw (Y) x3 = x * math.cos(w) + z2 * math.sin(w) z3 = -x * math.sin(w) + z2 * math.cos(w) # Roll (Z) x4 = x3 * math.cos(r) - y2 * math.sin(r) y4 = x3 * math.sin(r) + y2 * math.cos(r) return x4, y4, z3 def project_(x, y, z, scale=40): distance = 3 factor = scale / (z + distance) xp = int(WIDTH_/2 + x * factor) yp = int(HEIGHT_/2 - y * factor) return xp, yp cubeVertices_ = [ (-1,-1,-1), (1,-1,-1), (1,1,-1), (-1,1,-1), (-1,-1, 1), (1,-1, 1), (1,1, 1), (-1,1, 1) ] cubeEdges_ = [ (0,1),(1,2),(2,3),(3,0), (4,5),(5,6),(6,7),(7,4), (0,4),(1,5),(2,6),(3,7) ] def clearBuffer_(): for y in range(HEIGHT_): for x in range(WIDTH_): buffer_[y][x] = 0 def bufferToHex_(): hex_string = [] for y in range(HEIGHT_): for x in range(0, WIDTH_, 4): nibble = ( (buffer_[y][x] << 3) | (buffer_[y][x+1] << 2) | (buffer_[y][x+2] << 1) | (buffer_[y][x+3] << 0) ) hex_string.append(f"{nibble:X}") return "".join(hex_string) def draw3DCube(x, y, z): """ x = alpha (0–360°) → yaw y = beta (-180–180°) → pitch z = gamma (-90–90°) → roll """ yaw = -x pitch = -y roll = -z clearBuffer_() projected = [] for (vx, vy, vz) in cubeVertices_: xr, yr, zr = rotatePoint_(vx, vy, vz, pitch, roll, yaw) xp, yp = project_(xr, yr, zr) projected.append((xp, yp)) for a, b in cubeEdges_: x0, y0 = projected[a] x1, y1 = projected[b] drawLine_(x0, y0, x1, y1) return bufferToHex_() async def atk(dom): global lcd_, oled_ lcd_ = ucuq.ravel.LCD() oled_ = ucuq.ravel.OLED() await dom.inner("", BODY) async def atkDisplayOrientation(dom, values): x, y, z = (float(value) for value in values.split(",")) lcd_.backlightOn().moveTo(0, 0).putString(f"{f"{int(z):+3d} {int(x):4d} {int(y):+4d}".center(16)}".ljust(32)) oled_.draw(draw3DCube(x, y, z), 128).show() ATK_HEAD = """ <script> async function getOrientation() { if (!window.DeviceOrientationEvent || !window.DeviceOrientationEvent.requestPermission) { return alert("Your current device does not have access to the DeviceOrientation event"); } let permission = await window.DeviceOrientationEvent.requestPermission(); if (permission !== "granted") { return alert("You must grant access to the device's sensor for this demo"); } } var previousTime = 0; window.addEventListener("deviceorientation", function (e) { let x = Math.round(e.alpha); let y = Math.round(e.beta); let z = Math.round(e.gamma); document.getElementById('PartnerOrientationX').innerHTML = `${x}°`; document.getElementById('PartnerOrientationY').innerHTML = `${y}°`; document.getElementById('PartnerOrientationZ').innerHTML = `${z}°`; if ((performance.now() - previousTime) < 333) return; previousTime = performance.now(); launchEvent(`${x},${y},${z}|BUTTON|click||(DisplayOrientation)`); }); </script> """ BODY = """ <fieldset style="display: flex; justify-content: space-around;"> <legend>Orientation</legend> <span> <span>Z: </span> <output style="display: inline-block; font-family: monospace; width: 4ch; font-size: larger;" id="PartnerOrientationZ"></output> </span> <span> <span>X: </span> <output style="display: inline-block; font-family: monospace; width: 4ch; font-size: larger;" id="PartnerOrientationX"></output> </span> <span> <span>Y: </span> <output style="display: inline-block; font-family: monospace; width: 4ch; font-size: larger;" id="PartnerOrientationY"></output> </span> </fieldset> """ atlastk.launch(globals=globals())

Multi-events#

An app that makes LEDs flash and text scroll at a steady pace, but at different rates.

import ucuq DURATION = 15 RING_INTENSITY = 10 def buildRingEvents(ring, delay, color, positions): elapsed = 0 switch = False events = [] while elapsed < DURATION: events.append((lambda switch = switch: (tuple((ring.setValue(position, color if switch else (0,0,0))) for position in positions), ring.write()), delay)) switch = not switch elapsed += delay return events def buildLCDEvents(lcd, line, text, delay): elapsed = 0 events = [] text = text.rjust(16) position = 0 while elapsed <= DURATION: events.append((lambda position = position % 16: lcd.moveTo(0,line).putString(text[position:] + text[:position]), delay)) position += 1 elapsed += delay return events def main(): ring = ucuq.ravel.Ring() lcd = ucuq.ravel.LCD().backlightOn() allEvents = ( buildRingEvents(ring, 1, (RING_INTENSITY,0,0), (0,1)), buildRingEvents(ring, 2/3, (0,RING_INTENSITY,0), (2,3)), buildRingEvents(ring, 2/5, (0,0,RING_INTENSITY), (4,5)), buildRingEvents(ring, 2/7, (RING_INTENSITY,RING_INTENSITY,0), (6,7)), buildLCDEvents(lcd, 0, "3 scrolls/s", 1/3), buildLCDEvents(lcd, 1, "4 scrolls/s", 1/4) ) ucuq.sleepStart() ucuq.playEvents(allEvents, lambda _, cumul: ucuq.sleepWait(cumul)) ring.fill((0,0,0)).write() lcd.backlightOff().clear() main()

Troubleshooting