The Ravel kit

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

Troubleshooting