Le kit Ravel

Le kit Ravel #

Le kit Ravel

Pour une utilisation avec simulateur (plus d’explications ici) :

  • Simulation Wokwi ;
  • configuration: .

Bouton Run pour (re)lancer l’application.

Headless (sans interface graphique) #

RAZ #

Remise à zéro du kit

import ucuq ucuq.Ravel.raz()

Buzzer #

Beep #

import ucuq FREQ = 440 buzzer = ucuq.Ravel.Buzzer() buzzer.on(FREQ) ucuq.sleep(1) buzzer.off()

Gammes #

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

OLED #

Texte #

import ucuq, datetime oled = ucuq.Ravel.OLED() oled.text("Nous sommes le".center(16), 0, 20).text(f"{datetime.datetime.now().strftime("%d/%m/%Y %H:%M").center(16)}", 0, 40).show()

Dessin #

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

Anneau RGB #

Uni #

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 affichage #

import ucuq lcd = ucuq.Ravel.LCD() lcd.putString("Salutations !".center(16)).backlightOn()

Vagues #

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

Multi-composants #

Panthère rose #

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

Avec interface graphique #

Orientation #

Cette application ne fonctionne que sur un dispositif mobile (tablette, smartphone…) équipé des capteurs appropriés et affiche un cube 3D représentant l’orientation du dispositif.

Cette application doit être ouverte dans son propre onglet pour fonctionner (cliquer sur dans l’encart ci-dessous) !

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

Boilerplate (exemple de base) #

Une application minimaliste pour allumer/éteindre l’afficheur 7 segments (simulation) ou la DEL embarquée (kit physique).

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

Figures de Chernoff #

Application affichant des figures de Chernoff.

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 #

Un jeu inspiré de Simon.

NOTA : pour un utilisation avec le simulateur, décommenter (enlever le # et l’espace qui suit) l’expression SIMULATOR = True à la ligne 3.

import atlastk, ucuq, json, math, random # SIMULATOR = True 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 = 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)).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())

Pierre-feuille-ciseaux #

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 #

Le jeu du morpion.

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

Dépannage