生成AIでLispな魔法陣を作った話
前置きその1
昔から魔法、その中でも魔法陣ってプログラムに似ていると思っていました。ファンタジー作品でもそのように描かれることが結構多い気がします。
パッと思いつくのは古宮九時さんの描く『Unnamed Memory』や『Babel』に出てくる魔法(魔法陣)。
「構成」という技法でもって、緻密に組み上げられた図形と魔法文字を用いた魔法陣が幾度となく出てきて、解析したりされたり、編集したり、バグに悩まされたりパッチあててみたり。リバースエンジニアリングじゃないこれ? とか、デバックそのままだわー。なんて、読みながらたびたび思ったものでした。(ちなみに魔法文字は256種類、1オクテット。8ビットコードで記述できる模様デスw)
前置きその2
いっぽう、現実のプログラミング言語で魔法陣っぽいものというと、node-REDや
NoFlo なんかに代表されるフローベースのビジュアルプログラミング言語が近い気がします。
↑のNoFloのサンプルなんか結構いい線行ってますが、できればこのフロー構造が「データの流れ」より「論理構造」を志向していてほしいところ。(それでいて円形であると美的にのぞましいですね☆)
論理構造を美しく描ける(?)プログラミング言語と言えば、何と言ってもLispです(個人的偏見)
Lispはすごいよ なんでもできるよ メタメタ~♪ とか変な節回しの歌を紹介するよりまずはこの本をどうぞw
↑の吉田 武先生のLisp愛が詰まった(特に後半)名著です。
Webでもたくさん情報がでています。
↑このあたりではLisp入門した方が美しさに感動している様子が見受けられます。さすが「神の言語」たるLisp様ですね。
この、Lisp様がもつ神の構造、その美しさを、ビジュアル化できないものかしらん?
なんてことを、以前よりずーっと思っていたワタクシなのです。
前置きその3
で、ここのところの大流行の生成AIさん、けっこういい加減な答えをまことしやかにしれっと出してきやがるので、正直クリティカルな方面に使うのは厳しいかなあなんて思っていたのですけれども、ここで考えを改めまして、
「どうせ虚言癖のあるAIなんだったら、嘘でももっともらしいことを出力してもらえばいいんじゃない?」と思うことにしました。
つまり、どうせ嘘・虚構であるはずのSFやファンタジーの道具として、説得力強化グッズとしてつかうのには最適なんでは? ということです。
なんせ嘘でもやたら説得力のある答えを出してくれるのですから。
説得力のある虚構なんて、SFのキモと言ってもいいぐらいでしょう。そこがAIにやられてしまうと作家性はどうするのかとか別問題がでてきてこれはまた困るのですが、まあそこはそれ、いまは脇においておきましょう。
そう、どうせ嘘でも、もっともらしくあればいいんです。それを人間様が利用させていただければ御の字というわけ。
LLM (大規模言語モデル)の説得力ある嘘解答アンドLisp様のもつ構文解析が簡単なすてき構造と、最近めきめき伸びてきた画像加工AIを組み合わせたら、夢のアレができるんではないか? ということですね。
てなことで、
いよいよ本題! Lispプログラムを魔法陣風に描いてみる!
Lispのプログラム・コードを見ると、丸カッコ(”()”←S式と言います。)が異様にたくさんでてきます。これはこれで「美しさ」を体現しているので信者の目には愛おしく見えるのですが、一般の方にはなかなかこの美しさがわからないと思うので、AIさんの助けを借りてカッコを円として解釈して構文解析をおこなってみました。
もうちょいカッコよく言うと、LispのS式(カッコ構造)を円形・魔法陣的に視覚化してみたわけ。
サンプルで使うプログラムはプログラミングの練習問題としてよく知られているゲームのFizzBuzzでやってみることにしました。
Lispで雑に表すとこんなかんじでしょうか
(dotimes (i 100)
(cond ((and (= (mod i 3) 0) (= (mod i 5) 0)) (print "FizzBuzz"))
((= (mod i 3) 0) (print "Fizz"))
((= (mod i 5) 0) (print "Buzz"))
(t (print i))))
ちなみに、FizzBuzzとは、1から順番に数字を数えていく際に、3の倍数なら「Fizz」、5の倍数なら「Buzz」、3と5の両方の倍数なら「FizzBuzz」と表示するプログラムです。まあたいてい1~100までのループを作ってその中でループカウンタ変数を3と5で割ってあまりが0だったらってIF文をいれて。って感じで作ります。
そのお題を考えながら↑のリストを見ると、なんとなくLispってこんなかんじなのねってのがわかってもらえると思います。
なお、ホンモノの(?)Lisp信者だとまず1~100までのリストをつくってその中でmapを使ってリストを加工して、別のリストを返す。という手順になりそうですが、Lispの紹介ではなくて魔法陣化が今回の主題なのでそちらは脇に置いておきますw
※もっというと
(dotimes'101(format t"~&~[~;~A~v^~;~;Fi~;~;Bu~:;~v^FizzBu~]zz"(gcd .'15).'0))
というわずか77バイトでできちゃうらしいけどそれも割愛w
↑詳しくはこちらをどーぞ。
さて、閑話休題。話をもどして構造を図示してみると
LispなFizzBuzzの魔法陣構造(擬似図)
(mod i 3) (mod i 5)
\ /
(= (mod i 3) 0) (= (mod i 5) 0)
\ /
-------------------
| (and ...) |
-------------------
|
----------------
| cond |
| ------------ |
| | FizzBuzz | |
| | Fizz | |
| | Buzz | |
| | number | |
----------------
|
--------------
| dotimes (i 100) |
--------------
こんなかんじ。↑の下部から上に伸びていく順番の構造を円形にして、内側からひろがるように並べ替えると、真ん中に (dotimes (i 100) ...) があり、外側に向かって条件式が拡がっていくようにできます。
なんてことをAIちゃんに食わせて構造を図示してみると
[ Buzz ] [ Fizz ] [FizzBuzz] [ i ]
| | | |
(= (mod i 5) 0) (= (mod i 3) 0) (and ...) (print i)
\ / /
\ / /
(cond ...)
|
(dotimes (i 100) ...)
こうなり、さらにデザインのイメージを聞いてみると
ほほぉ。おらぁワクワクしてきたぞ!☆彡
てなこって、では実際にグラフィックにしてもらいましょう!
どーん!
なにこれかっこいい!!w
さらに手書き風では
これはこれで趣深いですね☆彡
Pythonスクリプト:Lisp魔法陣描画器
ここまでできたらAIさんがいなくても魔法陣を描けるようにしたい!
ってなことで、Pythonスクリプト化してみます。(というよりこれもAIにやってもらいましたw)
以下は、LispのS式を構文解析して、それを「同心円状に可視化(魔法陣風)」するPythonスクリプトです。
構文をツリーとしてパースし、各ネストレベルごとに円周上にノードを描画します。
import matplotlib.pyplot as plt
import math
import ast
# Lisp風のS式をPythonリストに変換(簡易)
def parse_lisp_expr(expr_str):
expr_str = expr_str.replace('(', ' [ ').replace(')', ' ] ')
tokens = expr_str.split()
stack = []
current = []
for token in tokens:
if token == '[':
stack.append(current)
current = []
elif token == ']':
temp = current
current = stack.pop()
current.append(temp)
else:
current.append(token)
return current[0] if current else []
# ノード位置を円形に決定
def get_positions(tree, level=0, positions=None, angle_start=0, angle_end=2 * math.pi):
if positions is None:
positions = []
n = len(tree)
angle_step = (angle_end - angle_start) / max(n, 1)
for i, elem in enumerate(tree):
angle = angle_start + i * angle_step
r = level * 2.5
x = r * math.cos(angle)
y = r * math.sin(angle)
label = str(elem) if not isinstance(elem, list) else ''
positions.append((x, y, label))
if isinstance(elem, list):
get_positions(elem, level + 1, positions, angle, angle + angle_step)
return positions
# 可視化
def draw_lisp_magic_circle(expr_str):
tree = parse_lisp_expr(expr_str)
positions = get_positions(tree)
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_aspect('equal')
ax.axis('off')
for x, y, label in positions:
ax.plot(x, y, 'o', color='black')
if label:
ax.text(x, y, label, ha='center', va='center', fontsize=10, bbox=dict(boxstyle="round", facecolor="white", alpha=0.6))
# 円装飾
max_level = max(math.hypot(x, y) for x, y, _ in positions)
for r in range(1, int(max_level) + 3, 2):
circle = plt.Circle((0, 0), r, color='gray', linestyle='dotted', fill=False)
ax.add_patch(circle)
plt.title("🔮Lisp Magic Circle (FizzBuzz)", fontsize=14)
plt.show()
# 実行:FizzBuzz(Common Lisp風)
fizzbuzz_code = """
(dotimes (i 100)
(cond ((and (= (mod i 3) 0) (= (mod i 5) 0)) (print "FizzBuzz"))
((= (mod i 3) 0) (print "Fizz"))
((= (mod i 5) 0) (print "Buzz"))
(t (print i))))
"""
draw_lisp_magic_circle(fizzbuzz_code)
描画にはmatplotlibを使うので、入っていないようなら
> pip install matplotlib
とかを事前にやっておく必要があります。
コードの最後の方の
fizzbuzz_code = """
(dotimes (i 100)
(cond ((and (= (mod i 3) 0) (= (mod i 5) 0)) (print "FizzBuzz"))
((= (mod i 3) 0) (print "Fizz"))
((= (mod i 5) 0) (print "Buzz"))
(t (print i))))
"""
の中身を書き換えると、FizzBuzz以外でもできます😃
さて、その実行結果は、
むむ、これはこれで趣深いけれど、なんだか偏っていますね。(もっと大きなプログラムなら円周にまんべんなく広がりそうですが)
なので、円周に等間隔に並べるように変更して。とAIに指示したら
これはちょっと違う。指示ミスです><
あらためて、さらに数回のやり取り(ここらへんがChatGPTさんのChatたる所以ですね)を経て、「構造は維持しつつ、同一レベル内で均等に円周上に配置したい」と指示して作られたのがこちら
ちょっと中心のdotimesがずれている気もしますが、まあよいかも?
これにルーン文字をちりばめてそれらしくして、ついでに各小円の中の文字も背景の円周と同角度にしてもらったのがこちらッ!!
それっぽい! かっこいいい!!(*´▽`*)
完成形Pythonプログラムリスト
import matplotlib.pyplot as plt
import math
import random
# Lisp風のS式をPythonリストに変換(簡易)
def parse_lisp_expr(expr_str):
expr_str = expr_str.replace('(', ' [ ').replace(')', ' ] ')
tokens = expr_str.split()
stack = []
current = []
for token in tokens:
if token == '[':
stack.append(current)
current = []
elif token == ']':
temp = current
current = stack.pop()
current.append(temp)
else:
current.append(token)
return current[0] if current else []
# レベル別にノードと接続を取得
def get_level_positions(tree, radius_step=2.5):
positions = []
connections = []
levels = {}
def recurse(subtree, level, parent_idx=None):
idx = len(positions)
label = str(subtree) if not isinstance(subtree, list) else ''
if level not in levels:
levels[level] = []
levels[level].append(idx)
positions.append((level, label))
if parent_idx is not None:
connections.append((parent_idx, idx))
if isinstance(subtree, list):
for elem in subtree:
recurse(elem, level + 1, idx)
recurse(tree, 0)
final_positions = [None] * len(positions)
for level, indices in levels.items():
angle_step = 2 * math.pi / len(indices)
radius = (level + 1) * radius_step
for i, idx in enumerate(indices):
angle = i * angle_step
x = radius * math.cos(angle)
y = radius * math.sin(angle)
lvl, label = positions[idx]
final_positions[idx] = (x, y, label, angle)
return final_positions, connections
# ルーン文字を円周上に配置
def draw_runes(ax, radius, num_runes=16):
rune_symbols = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','ᚲ','ᚷ','ᚹ','ᚺ','ᚾ','ᛁ','ᛃ','ᛇ','ᛈ','ᛉ','ᛋ']
for i in range(num_runes):
angle = 2 * math.pi * i / num_runes
x = radius * math.cos(angle)
y = radius * math.sin(angle)
rune = random.choice(rune_symbols)
ax.text(x, y, rune, fontsize=16, color='#bb88ff', ha='center', va='center',
rotation=math.degrees(angle)+90, family='Segoe UI Historic')
# 可視化
def draw_lisp_magic_circle(expr_str):
tree = parse_lisp_expr(expr_str)
positions, connections = get_level_positions(tree)
fig, ax = plt.subplots(figsize=(10, 10))
ax.set_aspect('equal')
ax.axis('off')
fig.patch.set_facecolor('#1e1e2f')
ax.set_facecolor('#1e1e2f')
# 線の描画
for start_idx, end_idx in connections:
x1, y1, *_ = positions[start_idx]
x2, y2, *_ = positions[end_idx]
ax.plot([x1, x2], [y1, y2], color='#8888ff', linewidth=1.0)
# ノードの描画
for x, y, label, angle in positions:
ax.plot(x, y, 'o', color='#ffdd55', markersize=6)
if label:
ax.text(x, y, label, ha='center', va='center', fontsize=10, color='#ffffff',
family='monospace', rotation=math.degrees(angle)+90,
bbox=dict(boxstyle="circle,pad=0.3", facecolor="#333344", edgecolor="#aaaaee", lw=0.8))
# レベル円の装飾とルーン
max_radius = max(math.hypot(x, y) for x, y, *_ in positions)
for r in range(2, int(max_radius) + 3, 2):
circle = plt.Circle((0, 0), r, color='#444477', linestyle='dashed', fill=False, linewidth=0.8)
ax.add_patch(circle)
draw_runes(ax, r + 0.3)
plt.title("🔮 Lisp Magic Circle – FizzBuzz (Structured + Runes)", fontsize=16, color='#ffffff')
plt.show()
# 実行:FizzBuzz(Common Lisp風)
fizzbuzz_code = """
(dotimes (i 100)
(cond ((and (= (mod i 3) 0) (= (mod i 5) 0)) (print "FizzBuzz"))
((= (mod i 3) 0) (print "Fizz"))
((= (mod i 5) 0) (print "Buzz"))
(t (print i))))
"""
draw_lisp_magic_circle(fizzbuzz_code)
そこそこ長くなっちゃいましたが、これを実行すれば、貴方のPCの画面に魔法陣が出力されるはずです!
もちろん、最後のところのfizzbuzz_codeを書き換えれば、FizzBuzz以外のプログラムも描画可能!
Lispは構文も簡単(ですよね?)で構造化も容易、それでもって世界を記述できるオブジェクト指向プログラミングにも対応しています。
皆様も現実を書き換えるプログラムを書いて、ぜひぜひこの宇宙はメモリ上に実装・記述されたものだというシミュレーション仮説を実証していただければと思いますw
終わりに
いい加減な思いつきと、虚言癖のある相棒(ChatGPT)と、久しぶりに予定のない日曜日の遊びとしては結構いいかんじに出来た気がします。
この後もChatGPTさんってば「エフェクトを入れてアニメーションもできます!」とか「回転させてみましょう!」とかいろいろ提案してくれて、あっという間に無料分のトークンを使いきってしまいましたw
ココから先は有料のようです。
(できれば角速度一定じゃなくて内側の軌道は早く回転するようにしてほしいなぁ(なんて希望は有料ですってw)
なお、プログラムで魔法陣というと
↑このあたりの数独みたいな見た目のモノになってしまうことが多いのですが、やっぱりファンタジー好きなら円形構造をもったやつがいいですよね。
今回作ってみたLisp魔法陣プログラムで素敵な魔法陣を描いて、前置きに書いたような小説のネタにしてもよし、自作の魔法陣を眺めながらプログラムをデバッグするもよし(逆に難しいと思うけれど、魔法陣の図から「構成」を解析する遊びも楽しそうです)
将来的には各ノードをマウスでぽちってドラックして構造を変化させたりプログラム自体をビジュアルに編集できたらうれしいなあ。なんて思いますが、そこまでやるかは未定ですw
と、書いたところでこんな記事を見つけてしまいました。
うわ、これはこれで面白そうw
次の暇な休日(いつのなるのか?)はこのあたり遊びにいっているかもです?w
追記:
↑捕捉というか、Lisp様のすごさをちょいと連続で書いてみました☆彡
いいなと思ったら応援しよう!

コメント
4カァ、カァ、カァ、クダァ、クダァ...
久しぶりにLisp (List Processer)を耳(目?)にしました。
再帰処理や、メタ記述・処理などは得意だとかで、
大昔人工知能のアセンブラとか呼ばれて、一時期もてはやされていた記憶があります。
そういや第5世代の頃はPrologなんて言語もありましたっけ。
主記憶が1MB程度(Macintosh Plus)の頃Lisp の実装は難しくて、最低数メガ必要とかでグヌヌとした悲しい記憶があります。
今でもメインエディタがEmacs なので、何気に接している言語でもあるんですが...
Mattakaさん、さすがです!!
vi派なので実はLispはちょっと縁遠くてあこがれの言語であったりするのですw
もうちょっと使いこなしていたら、Pythonなんか(失礼w)使わないでLispの構文をLispで解析して描画までもっていけたのでしょうけれどねえw
調べてみたら最近はJavaのライブラリを利用できるような実装もあるそう。(でも試そうと思ったらリンク切れまくっていてワタクシがキレました><)
このあたり整備されたらもうちょっとメジャーになってくれるんじゃないかとおもうけれど、まだやっぱりPythonとかに比べると格段と不便なんですよねー><
Lisp の大きな問題の一つがやたらカッコが多くて可読性が低い事があると思うので、こういった魔法陣的・幾何学的な解釈・表示は、実は大きな意味があるんじゃないかと今回思いました。
お、この節点は面白そうと中を開くとマンデルブローみたいに無限に掘り下げていけて、つまんだりひっぱったりする事でAI的知能や性格、反応を変えられると面白いなと空想してしまいました。
私も最近Geminiを相棒(エキスパートシステム)として、実用的スクリプト作成をしていたりします。割と"動かねーじゃねーか!"というコードを吐くので、宥めすかしながら目的のコードに辿り着く事に面白さを感じています。w
Mattakaさま、そうそう! それですそれ!(∩´∀`)∩☆
せっかくビジュアライズしたのだから、拡大縮小回転ぐりんぐりん、ノードに分け入ってステップイン、ステップオーバーで次のノードに……などなどできたら面白そうですよねーw 夢はひろがりますw ひろがるだけで実装できるわけじゃないですけどw
このあたりAIさんが助けてくれたらいいのに、まだまだもうちょい進化が必要っぽいですね><
とりあえず、Lisp様のすごいところはまた記事にしてみましたのでこちらもどぞー。
https://note.com/rasen/n/naebb459de48d
※Geminiを相棒にされてるなら、noteさんが公式にがそういう記事求めてるようですよー? 賞金つきらしいので参加されてみてはどうでしょう? 私の魔法陣ネタはChatGPTなのでエントリーできませんでしたけどw
https://note.com/info/n/n516d13410090