🗝️
動くコード小説「暗号アリス」
Cipher Alice — Executable Code-Novel
Run this file with Python 3.
You can tweak RUN_MODE and ENDING_CHOICE at the bottom.
from textwrap import dedent
import random
# ===== Utilities =====
class DecodeError(Exception):
pass
def rot13(s: str) -> str:
# ROT13 for ASCII letters only
result = []
for ch in s:
o = ord(ch)
if 65 <= o <= 90: # A-Z
result.append(chr((o - 65 + 13) % 26 + 65))
elif 97 <= o <= 122: # a-z
result.append(chr((o - 97 + 13) % 26 + 97))
else:
result.append(ch)
return ''.join(result)
def bin_to_text(bits: str) -> str:
try:
bits = bits.replace(' ', '').strip()
if not bits:
return ""
if len(bits) % 8 != 0:
raise DecodeError("binary length must be multiple of 8")
chars = [chr(int(bits[i:i+8], 2)) for i in range(0, len(bits), 8)]
return ''.join(chars)
except Exception as e:
raise DecodeError(f"invalid binary: {e}")
def show(title: str, body: str) -> str:
# Simple pretty printer for chapters
bar = "=" * len(title)
return f"{title}\n{bar}\n{dedent(body).strip()}\n"
# ===== Characters =====
class Alice:
def __init__(self):
self.name = "有栖"
self.role = "LUCID" # 夢を見る者
self.memory = []
self.notes = []
def perceive(self, text: str) -> str:
"""Try to 'read' ciphered text."""
try:
return self.decode(text)
except DecodeError as e:
return f"[ERROR] {e}"
def decode(self, text: str) -> str:
"""A playful decoder that supports several formats."""
# Simple routes:
table = {
"010010": "君はもう夢の中だよ",
"101101": "君はまだ夢の外だよ",
}
if text in table:
return table[text]
# Try binary (8-bit)
if set(text) <= {"0", "1", " "} and len(text.replace(" ", "")) >= 8:
return bin_to_text(text)
# Try ROT13 if looks ASCII-ish
if all(ord(c) < 128 for c in text) and any(c.isalpha() for c in text):
return rot13(text)
raise DecodeError("解読不能な暗号列")
class Claude:
def __init__(self):
self.identity = "転校生"
self.hidden = "SystemGuide"
def guide_line(self) -> str:
return "クロード: 君はルシッドだ。世界の裏側を見てしまった。"
class Cheshire:
def __init__(self):
self.smile = True
def talk(self) -> str:
# Randomly flip between two messages
return random.choice(["010010", "101101"]) # inside vs. outside
class QueenOfHearts(Exception):
def __init__(self):
super().__init__("――あなたは、誰の夢を見ているの?")
# ===== Chapters =====
def chapter0_prologue(alice: Alice) -> str:
body = f"""
教室の光が砕け、教科書の文字列が暗号へと崩落する。
{alice.name}: 「これ……全部、記号に見える」
黒板 = /\\/\\/\\/\\
声 = [ノイズ][ノイズ][ノイズ]
"""
alice.memory.append("世界の裏面を見た")
return show("序章: 鍵穴", body)
def chapter1_gate(alice: Alice, claude: Claude) -> str:
line = claude.guide_line()
body = f"""
{line}
扉は地下鉄のホームの隙間にあった。黄色い線の向こう、世界の境界のような薄い影。
有栖の指先は鍵穴の輪郭をなぞる。金属ではない、記号でできた冷たさ。
// KEY = rot13("Nqnz vf ybir") → {alice.perceive("Nqnz vf ybir")}
"""
alice.memory.append("境界を越えた")
return show("第一章: 暗号都市への招待", body)
def chapter2_city(alice: Alice) -> str:
signs = [
('"Uryyb, Jbeyq"', alice.perceive("Uryyb, Jbeyq")), # Hello, World
('"Zl Anzr vf Fvtangher"', alice.perceive("Zl Anzr vf Fvtangher")), # My Name is Signature
]
body = f"""
扉の先には、記号で呼吸する街が広がっていた。信号は二進数で瞬く。
看板A: {signs[0][0]} → {signs[0][1]}
看板B: {signs[1][0]} → {signs[1][1]}
市民の足音は点と線のモールスに砕け、空の広告は関数として落ちてくる。
有栖: 「読める……読めてしまう」
"""
return show("第二章: 暗号都市", body)
def chapter3_cheshire(alice: Alice, cat: Cheshire) -> str:
msg = cat.talk()
say = alice.perceive(msg)
body = f"""
路地の曲率がふいに反転し、猫耳のアンドロイドが笑った。
チェシャ: {msg} → {say}
有栖: 「どっち? 夢の中? 夢の外?」
チェシャ: rot13("V qba'g zrrg crbcyr. V znxr crbcyr.")
解読 → {alice.perceive("V qba'g zrrg crbcyr. V znxr crbcyr.")}
"""
return show("第三章: チェシャ", body)
def chapter4_trial(alice: Alice) -> str:
try:
raise QueenOfHearts()
except QueenOfHearts as q:
qline = str(q)
# trial transcript (corrupted)
transcript = "[DATA_CORRUPT] [DECODER_FAILED]"
body = f"""
王城は円環の法廷で、証言は常に別の現実に分岐する。
女王: {qline}
記録官: {transcript}
有栖(心の声): 「答えは、だれの夢でもあって、わたしの夢でもある」
"""
alice.memory.append("問いを受けた")
return show("第四章: 万華鏡裁判", body)
def chapter5_choice(alice: Alice, ending_choice: str) -> str:
# Note sneaks a hidden message regardless of choice
note_bin = "01001011 01101111 01101101 01100101" # Come
note_text = alice.perceive(note_bin)
if ending_choice == "return_to_reality":
body = f"""
クロード: 「ここで選べ。有栖」
有栖: 「……戻る。現実へ」
シーンは白いノイズに溶け、教室の午後へ復帰する。
だがノートの余白には二進数の走り書き―― {note_bin} → {note_text}
"""
outcome = "エンディングA: 目覚め"
else:
body = f"""
クロード: 「ここで選べ。有栖」
有栖: 「……残る。物語の中に」
都市は花粉のような文字列を撒き散らし、彼女の輪郭は物語の行末へ吸い込まれていく。
それでもノートは彼女のポケットにありつづけた―― {note_bin} → {note_text}
"""
outcome = "エンディングB: 夢の居住"
alice.notes.append(note_text)
return show(outcome, body)
# ===== Runner =====
def run_story(seed: int = 42, mode: str = "LN", ending_choice: str = "return_to_reality") -> str:
"""
mode:
- "LN": readable light-novel leaning output
- "EXP": more glitchy experimental inserts
ending_choice: "return_to_reality" or "stay_in_dream"
"""
random.seed(seed)
alice = Alice()
claude = Claude()
cat = Cheshire()
chapters = [
chapter0_prologue(alice),
chapter1_gate(alice, claude),
chapter2_city(alice),
chapter3_cheshire(alice, cat),
chapter4_trial(alice),
chapter5_choice(alice, ending_choice),
]
text = "\n".join(chapters)
if mode == "EXP":
# inject some experimental noise blocks
noise_blocks = [
'\n```\n/\\/\\/\\/\\\n0101 ΔΔΔ / ? ? ?\n[NOISE][NOISE][NOISE]\n```\n',
'\n// glitch: rot13("Gur jbeyq vf n qrnq znfgre") → ' + alice.perceive("Gur jbeyq vf n qrnq znfgre"),
]
text += "\n".join(noise_blocks)
# closing memory dump
memory_dump = "\n".join(f"- {m}" for m in alice.memory)
notes_dump = "\n".join(f"- {n}" for n in alice.notes) if alice.notes else "- (none)"
text += "\n" + show("Appendix: 有栖の記憶", memory_dump)
text += "\n" + show("Appendix: ノートの断片", notes_dump)
return text
if __name__ == "__main__":
# Tweak here:
RUN_MODE = "LN" # "LN" or "EXP"
ENDING_CHOICE = "return_to_reality" # "return_to_reality" or "stay_in_dream"
SEED = 123
output = run_story(seed=SEED, mode=RUN_MODE, ending_choice=ENDING_CHOICE)
print(output)
Discussion