🗝️

動くコード小説「暗号アリス」

に公開

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