🖍️ Darkwaar27 : Pigment — Un casse-tête avec des dés basé sur la rotation et l'algèbre des couleurs
Un dé, une règle
Pigment est un casse-tête déterministe se jouant sur un plateau de 5×5. Un dé occupe une case du plateau. Une commande le déplace vers une case voisine orthogonale et le fait pivoter de quatre-vingt-dix degrés afin qu’une nouvelle face soit tournée vers le bas. Cette face tournée vers le bas colore la case de destination. Rien d’autre ne bouge.
Le joueur ne choisit jamais de couleur dans un menu. La couleur qui apparaît est celle de la face sur laquelle le dé s’est arrêté après avoir été pivoté par le chemin tracé. Les quatre directions s’apparentent à un calcul d’itinéraire ; elles redéfinissent également les faces qui seront disponibles par la suite. C’est ce couplage qui constitue le jeu.
Cinq niveaux créés par l’auteur, une limite de coups par plateau, des fonctions « annuler » et « recommencer ». Aucun élément aléatoire et aucun autre acteur.
Modélisation d’un dé à six faces
Le dé est constitué de six entiers associés à chaque face : D, U, N, S, E, W. Les opposés sont fixes : nul↔noir, rouge↔bleu, jaune↔blanc. Chaque niveau commence avec la même orientation :
DirectionFaceBasNullHautNoirNordRougeSudBleuEstJauneOuestBlanc
Une rotation est une permutation de ces six emplacements. L’est et l’ouest laissent le nord et le sud inchangés ; le nord et le sud laissent l’est et l’ouest inchangés. Au départ, la première face « Est » est jaune, la première face « Sud » est bleue, la première face « Nord » est rouge et la première face « Ouest » est blanche. Les lancers suivants déplacent ces faces.
static func roll(die: Dictionary, action: String) -> Dictionary: var d := int(die["D"]) var u := int(die["U"]) var n := int(die["N"]) var s := int(die["S"]) var e := int(die["E"]) var w := int(die["W"]) match action: "E": return {"D": e, "U": w, "N": n, "S": s, "E": u, "W": d} "W" : return {"D" : w, "U" : e, "N" : n, "S" : s, "E" : d, "W" : u} "N" : return {"D" : n, "U" : s, "N" : u, "S" : d, "E" : e, "W" : w} "S" : return {"D" : s, "U" : n, "N" : d, "S" : u, "E" : e, "W" : w} _ : return {"D" : d, "U" : u, "N" : n, "S" : s, "E" : e, "W" : w}
Il s’agit d’une permutation d’états, et non d’une simulation physique. Il n’y a ni vitesse angulaire, ni réaction aux collisions, ni maillage de surface. Le cube à l’écran est un dessin représentant ces six entiers une fois que les règles ont accepté la pointe. Un contrôle d’invariance garantit que les opposés restent cohérents : si D est rouge, U doit toujours être bleu.
Algèbre des couleurs
Les taches forment un petit alphabet : vide, rouge, jaune, bleu, blanc, noir, orange, violet, vert. La face d’atterrissage écrit la cellule de destination via une fonction déterministe de (tache_existante, face_d’atterrissage) :
static func mix(existing: int, face: int) -> int: if face EMPTY: return existing if face WHITE: return EMPTY if face BLACK: return BLACK if existing EMPTY: return face if existing == face: return existing var low := mini(existing, face) var high := maxi(existing, face) if low RED and high YELLOW: return ORANGE if low RED and high BLUE: renvoie VIOLET si low JAUNE et high BLEU : renvoie VERT renvoie NOIR
Les couleurs primaires peignent les cases vides et se mélangent pour former des couleurs secondaires. L'application deux fois de la même couleur primaire n'a aucun effet. Le blanc efface tout, y compris une case scellée. Le noir scelle. Une couleur primaire appliquée sur une couleur secondaire, ou sur une case scellée, la scelle. La valeur null laisse la case inchangée — une rotation libre qui consomme tout de même un coup.
Le mélange l’emporte sur la réécriture, car la case se souvient de la première couleur primaire. Appliquer la seconde moitié d’une paire implique de dépenser d’autres cases lors des tours qui ont mis cette face vers le bas. Le plateau est à la fois une toile et une boîte de vitesses.
Une action copie l’état, refuse les directions illégales, effectue un lancer, mélange, puis classe la phase :
static func apply(state: Dictionary, action: String) -> Dictionary: var current := snapshot(state) if str(current["phase"]) != "play" or not DIRS.has(action): return {"state": current, "events": {"accepted": false, "action": action}} var from: Vector2i = current["pos"] var to: Vector2i = from + DIRS[action] if not floor(current, to): return {"state": current, "events": {"accepted": false, "action": action, "from": from}} var die : Dictionary = roll(current["die"], action) var stains : Array = current["stains"] var at := index(to) var before := int(stains[at]) var after := mix(before, int(die["D"])) stains[at] = after current["die"] = die current["stains"] = stains current["pos"] = to current["spent"] = int(current["spent"]) + 1 # la phase devient « gagnée » ou « perdue » en fonction des marques, de la limite et des conseils légaux
Les conseils hors plateau sont refusés sans dépenser de coup. Une marque scellée ne constitue pas un blocage définitif immédiat : le blanc peut toujours la supprimer à l’intérieur de la limite, et la fonction « undo » rétablit l’orientation et la marque précédentes.
Position Ă— Orientation Ă— Pigment
La recherche de chemin vérifie si une case est accessible. Le pigment vérifie si une case est accessible avec une face spécifique tournée vers le bas, après un historique de taches spécifique. L’état complet est le produit de :
-
Position sur le 5Ă—5
-
de l’orientation du dé (six faces, contrainte par les opposés)
-
de vingt-cinq valeurs de coloration
-
des coups utilisés par rapport à la limite de niveau
Deux chemins menant à la même case divergent lorsque le dé arrive avec une face différente, ou lorsqu’un indice antérieur a laissé le mauvais pigment sur une marque.
Le niveau 2 (Retour) concrétise ce produit. Départ à (0, 0). La seule marque est (1, 0), verte. Limite 5. Le vert n’est pas présent sur le dé ; le jaune et le bleu doivent se rencontrer sur cette case.
ÉtapePointeTerrainFace vers le basEffet sur (1, 0)1Est(1, 0)JauneVide → Jaune2Sud(1, 1)Bleula case non marquée devient bleue3Ouest(0, 1)NulPas de couleur4Nord(0, 0)JauneLa case de départ devient jaune5Est(1, 0)BleuJaune + Bleu → Vert
Les étapes 2 à 4 servent principalement à faire pivoter le tableau. La case bleue sous la marque est une preuve fournie par le joueur, pas un objectif. Une autre ligne de longueur 5 rend également (1, 0) verte : est, sud, est, nord, ouest. Le niveau 2 accepte les deux. Le niveau 3 ajoute une deuxième marque — la case de départ doit être jaune — et la deuxième ligne échoue. Même case verte, mais les cases restantes ne sont pas les bonnes. La liste des marques constitue l’intégralité de la condition de victoire ; les cases non marquées peuvent prendre n’importe quelle couleur à la fin.
Cinq niveaux formant une échelle de contraintes
Chaque plateau ajoute une contrainte au même mécanisme :
-
Première face — placer du jaune sur (1, 0) en un seul coup. Pas de mélange.
-
Retour — mélanger le vert en repassant par certaines cases. Limite de 5.
-
Ce que vous laissez — du vert sur la marque et du jaune au départ. Mêmes cinq coups que pour le niveau 2 ; la ligne verte alternative ne respecte pas la marque supplémentaire.
-
L’autre paire — de l’orange en (1, 1). Le rouge et le jaune doivent se rencontrer. Limite : 6. Peindre la marque une seule fois puis s’arrêter ne permet pas d’obtenir de l’orange.
-
Blanc — (2, 0) commence scellé. Les marques exigent du jaune sur (1, 0) et que la case scellée soit vide. Le blanc doit être pivoté sur la case scellée. Éviter la case scellée ne permet pas de gagner.
Les limites de coups correspondent aux longueurs les plus courtes des solutions connues ; un détour doit donc remplacer des étapes plutôt que de les compléter. La victoire est vérifiée après chaque conseil accepté : la tache de chaque marque correspond à sa cible. La défaite survient lorsque la limite est épuisée avec une marque erronée, ou lorsqu’il ne reste plus de conseil valide avec une marque erronée.
Les cas de relecture testent les tables : les opposés restent cohérents, les chemins créés atteignent la phase attendue, les marques se maintiennent en cas de victoire, les refus de coin ne consomment pas de coups, et le niveau 5 commence scellé. Il s’agit d’une régression par rapport aux lignes connues, et non d’une preuve exhaustive d’unicité.
La couleur violette figure dans la table de mélange et n’est pas utilisée par ces cinq plateaux. La règle est conservée afin que l’alphabet ne change pas si un plateau ultérieur en a besoin.
Règles séparées de l’affichage
L’exécution se divise clairement :
ÉlémentResponsabilitérules.gdOrientation, mélange, données de niveau, victoire/défaite, vérifications de relecturemain.gdEntrée, _draw, interpolation, interface utilisateur, pile d’historiquemain.tscnUn seul Control
rules.gd est un RefCounted contenant des fonctions statiques. Il n’effectue jamais de dessin. main.gd contient le dictionnaire actuel, enregistre un instantané en profondeur avant l’acceptation d’une nouvelle pièce, puis lance une interpolation. La vue n’invente jamais la face suivante.
Les transitions sont des fonctions déterministes de l’état et de l’action ; la vue est une projection. Le programme n’est pas purement fonctionnel : les dictionnaires et les tableaux sont modifiés après une copie d’instantané, et la fonction « Annuler » utilise une pile d’historique explicite plutôt que des données persistantes. Cette séparation suffit à garantir la testabilité de la table de permutation sans avoir recours à un arbre de scène composé de nœuds de faces.
Pas d’ECS, pas de corps physiques, pas de shaders, pas de générateur procédural. La fonction « Annuler » retire un élément de l’historique et exécute une interpolation inverse plus courte. La fonction « Redémarrer » reconstruit start_level(index) et efface la pile. Les deux fonctionnent après une perte de données, ce qui permet de récupérer une marque scellée.
Rendu de l’état
Le plateau est constitué d’une couche de pigment uniforme sur un fond sombre. Les marques sont composées d’un anneau et d’une lettre (Y, G, O, …) ; l’objectif n’est donc pas uniquement indiqué par la teinte. Les cases scellées utilisent un remplissage sombre et un contour fin.
Le dé est un cube pseudo-3D : six quadrilatères, un biais de caméra, un algorithme de peinture en fonction de la profondeur, des glyphes sur la face supérieure. Lors d’un déplacement vers le haut, les positions sont interpolées, le cube s’élève selon un arc sinusoïdal et une Basis pivote de k · π/2 autour de l’axe de déplacement. La tache de destination effectue une interpolation entre la couleur précédente et la nouvelle avec le même poids. Un mélange correspond à un changement de couleur. Le blanc clignote sur le papier, puis se pose au sol.
Sous le plateau, un compas de nuanciers montre la face inférieure et les quatre côtés — le dé posé à plat. L’orientation est la ressource ; le dessin doit rendre la face visible lisible, sinon le mélange semble arbitraire.
Les commandes sont les flèches, les touches WASD, les tapotements sur les cellules voisines et un pavé tactile à l’écran. Pendant qu’une interpolation s’exécute, les pointes sont ignorées afin que l’image ne prenne pas de retard par rapport aux règles.
Ce qu’implique le modèle
Position × orientation × taches : voilà un vaste espace d’états issu d’un petit ensemble de règles. Les conseils nuls consomment un tour pour pivoter sans peindre. Les tableaux déterministes restent difficiles à parcourir à l’œil nu : la ligne verte en cinq étapes est évidente une fois qu’on l’a vue, mais opaque avant. L’animation qui nomme une règle — arc de pointe, changement de tache, boussole — fait partie de l’interface, ce n’est pas une décoration.
La campagne est finie : cinq plateaux, le violet n’est pas utilisé, pas de score. L’objet intéressant reste le dé : pièce et peinture, avec l’orientation comme mémoire. Tout le reste n’est qu’un tableau et une interpolation.
💫 Bluesky Creator - @alexdaubois : Réflexions sur l'équipe de sécurité PHP
💫 Créateur Reddit - r/opensource : QGIS instaure un moratoire sur les contributions liées à l'IA
đź’« GitHub Creator - Symfony: symfony/messenger v6.4.48
🤖 Veille Darkwood - 2026-10-14
đź’« Bluesky Creator - @martinfowler.com : Agents de programmation : faut-il les exploiter ou non ?
đź’« GitHub Creator - envoyproxy: v1.39.4
🤖 Veille Darkwood - 2026-10-13
🤖 Veille Darkwood - 2026-10-12
💫 Fondateur de Hacker News : le troisième centre de données de Yandex Cloud a été piraté
💫 Créateur Reddit - r/opensource : Gérer des dépôts GitHub sans contrôler tous les paramètres
🤖 Veille Darkwood - 2026-10-11
💫 Fondateur de Hacker News : Cloudflare rachète Deno
🤖 Veille Darkwood - 2026-10-10
🤖 Veille Darkwood - 2026-10-09
💫 Le créateur de Hacker News - OpenAI retire trois articles mathématiques
🤖 Veille Darkwood - 2026-10-08
💫 Créateur de Hacker News - Partager les avancées de l'IA en mathématiques
💫 Créateur Reddit - r/opensource : Game of Ur - Disponible sur Windows, Linux (Arch, Debian)
🤖 Veille Darkwood - 2026-10-07
💫 Créateur de Reddit - r/opensource : Rego vous dit « oui » ou « non » - Ergo vous explique pourquoi