0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FC版女神転生IIの「コードブレイカー」に特化して数字を頑張って当てるためのコード

Last updated at Posted at 2025-08-18

はじめの前のおねがい

できれば「いいね♡」をお願いします。励みになります。

はじめに

  1. このコードはFC版女神転生IIの「コードブレイカー」に特化して数字を頑張って当てるためのコードです。
  2. 検証サイトはいくつかありますが、ヒット&ブロー(3桁または4桁の数字を当てるやつ)を頑張って当てるためのコードを元に作成して、そこに女神転生2(FC版)コードブレイカー(ナンディさん)を攻略・考察するのページにある数値の偏りを参考にしたものを繋ぎ合わせたものです。
    (上記元リンクはうめほんのメモさんからですが、この記事を書かれた方の詳細は分かりませんでした。この場でお礼申し上げます)
  3. このコードはPython 3Pythonista 3に対応しています。

本コードを実行するとどうなるか

基本動作

本コードは3桁の数字を当てるためのコードで、

FC版女神転生II特化型コードブレイカー・ソルバー
入力形式:XYZ:HB  例)198:10 は 1Hit 0Blow/コマンド:prior/reset/undo/quit

推奨: 475  (直撃確率 ≈ 6.0%|候補 720)
1>

という画面が出てくるので、入力例に従って475:00のように「3桁数字:2桁数字」を入れていきます。この場合は「475という数字に挑戦したら、0ヒット、0ブローだった」という意味を指します。

つまり、実行すると入力画面になるので

  1. 半角で3桁の数字を入れる
  2. 直後をコロン(:)で区切る
  3. その後にヒット数、ブロー数の順に数字を入力する

のルールで入力します。

カスタマイズ

コードの最初の部分に、3桁にするか4桁にするかを切り替える部分と数字が重複するかどうかを設定できる部分があります。具体的にはソースコードの

CODE_LENGTH = 3                 # 3桁固定(このコードはFC版女神転生II特化型のため、数値をいじることはありません。改変前のコードの残滓です。4桁でも動きますがあまり意味はないです)
ALLOW_DUPLICATES = False        # 重複禁止(このコードはFC版女神転生II特化型のため、数値をいじることはありません。改変前のコードの残滓です。Trueでも動きますがあまり意味はないです)
TEMPERATURE = 0.15              # softmax 温度(0 で決定論)
HIT_BIAS = 0.05                 # 直撃バイアス λ:score += λ·log2 P(g)
SMOOTHING = 1e-9                # ラプラス平滑化(未観測コードのゼロ確率を避ける)
ALPHA_PRIOR = 1.0               # 一様×(1−α)+実測頻度×α(“最も高める”ので 1.0)
PRIOR_GAMMA = 2.2               # 実測頻度の強調指数(>1 で観測コードをさらに強調)
RNG_SEED: int | None = None     # 任意(例: 025)

の部分をいじります。

ルール

FC版「女神転生II」準拠です。

Pythonの必要なモジュール

ありません。

ソースコード

megaten2_solver.py
"""
FC版「女神転生II」特化型コードブレイカー・ソルバー

【操作方法】
・入力は「XYZ:HB」の形式です。例:198:10 は 1Hit 0Blow。
・数値は3桁の数字で重複禁止。
・各手で 「推奨: XXX(直撃確率…|候補 n)」を表示します。

【コマンド】
・prior —— 現在の事後分布の上位 10 件を “コード:確率%” で表示。
・reset —— セッションを初期化(候補・事前分布をリセット)。
・undo —— 直前の 1 入力を取り消し。
・quit / q / exit —— 終了。
"""

from __future__ import annotations

import math
import random
from collections import defaultdict
from itertools import permutations
from typing import Dict, List, Sequence, Tuple

# ─────────────────────────────────────────────────────
# 定数・ハイパーパラメータ
CODE_LENGTH = 3                 # FC版女神転生IIでは3桁固定
ALLOW_DUPLICATES = False        # FC版女神転生IIでは重複禁止
TEMPERATURE = 0.15              # softmax 温度(0 で決定論)
HIT_BIAS = 0.05                 # 直撃バイアス λ:score += λ·log2 P(g)
SMOOTHING = 1e-9                # ラプラス平滑化(未観測コードのゼロ確率を避ける)
ALPHA_PRIOR = 1.0               # 一様×(1−α)+実測頻度×α(“最も高める”ので 1.0)
PRIOR_GAMMA = 2.2               # 実測頻度の強調指数(>1 で観測コードをさらに強調)
RNG_SEED: int | None = None     # 任意(例: 025)

# 候補消滅(矛盾)時の既定動作:'reset' で初期化/'undo' で直前へ巻戻し
CONTRADICTION_BEHAVIOR = 'reset'  # 'reset' または 'undo'

# ─────────────────────────────────────────────────────
# 観測頻度(初手・実機想定)— “今までのやり取り” を完全反映
FREQ1: Dict[str, int] = {
    # 想定第1候補
    "186": 9, "187": 5, "198": 11, "254": 4, "274": 7, "372": 11,
    "475": 11, "546": 2, "578": 9, "654": 4, "681": 1, "691": 3,
    "716": 8, "785": 9, "814": 6, "864": 6, "876": 7, "987": 8,
    # 想定第2候補
    "129": 2, "167": 2, "213": 2, "217": 3, "231": 2, "316": 1,
    "321": 4, "367": 1, "491": 1, "496": 1, "537": 2, "541": 4,
    "586": 1, "659": 4, "679": 4, "798": 1, "863": 5, "918": 1,
    "942": 3, "961": 5, "985": 2,
}

# 2回目集合の無条件候補(次セッション初手で頻出した群も prior に取り入れて底上げ)
FREQ_NEXT_FLAT: Dict[str, int] = {
    "024":2, "034":4, "092":3, "120":1, "204":1, "239":2, "257":1,
    "290":1, "298":1, "392":1, "395":1, "396":2, "409":1, "430":1,
    "518":1, "529":1, "542":1, "592":1, "598":1, "625":2, "674":1,
    "687":1, "694":1, "695":1, "698":1, "702":2, "703":1, "762":1,
    "847":1, "851":1, "864":1, "905":3, "908":1, "918":1, "925":1,
    "926":1, "971":1, "981":1,
}

# ★ 二手目の“034”を強く優先:〔198, 578, 785, 876〕の直後に 034 が頻出という実測を反映
SECOND_MOVE_TRIGGER_SEEDS = {"198", "578", "785", "876"}  # 先行手のトリガ集合
SECOND_MOVE_TARGET_BONUS = {"034": 4}                     # 次手で特に推したいコード群
SECOND_MOVE_BONUS_LAMBDA = 2.0                            # スコア加点(log2=bits 単位)

# ─────────────────────────────────────────────────────
# 基幹部分

def all_codes() -> List[str]:
    """720通り(10P3)を生成。先頭0を許容・重複禁止。"""
    digits = "0123456789"
    return ["".join(p) for p in permutations(digits, CODE_LENGTH)]

def hb(secret: str, guess: str) -> Tuple[int, int]:
    """Hit & Blow(H:位置一致, B:値一致からH控除)。"""
    h = sum(1 for i in range(CODE_LENGTH) if secret[i] == guess[i])
    b = sum(min(secret.count(d), guess.count(d)) for d in set(guess)) - h
    return h, b

def normalize(d: Dict[str, float]) -> Dict[str, float]:
    s = sum(d.values())
    if s <= 0.0:
        n = len(d) or 1
        return {k: 1.0/n for k in d}
    return {k: v/s for k, v in d.items()}

# 観測コードを最も高める prior 構築(720通りは常に保持)
def build_initial_prior(universe: Sequence[str]) -> Dict[str, float]:
    """
    prior = (1−α)·Uniform + α·Amplified(Empirical)
    Empirical は FREQ1 と FREQ_NEXT_FLAT を合成し、指数 PRIOR_GAMMA で強調。
    未観測コードにも SMOOTHING を与え、候補性は維持(ゼロにはしない)。
    """
    n = len(universe)
    uniform = 1.0 / n if n > 0 else 0.0

    # 合成重み:FREQ1 と FREQ_NEXT_FLAT を総量でバランス
    sum1 = sum(FREQ1.values()) or 1.0
    sum2 = sum(FREQ_NEXT_FLAT.values()) or 1.0
    scale2 = sum1 / sum2  # 次集合の母数が小さいため寄与を補正

    raw: Dict[str, float] = {}
    for code in universe:
        w = 0.0
        if code in FREQ1:
            w += FREQ1[code]
        if code in FREQ_NEXT_FLAT:
            w += scale2 * FREQ_NEXT_FLAT[code]
        w = (w + SMOOTHING)
        raw[code] = w ** PRIOR_GAMMA  # 強調
    empirical = normalize(raw)

    alpha = max(0.0, min(1.0, ALPHA_PRIOR))
    prior: Dict[str, float] = {}
    for code in universe:
        prior[code] = (1.0 - alpha) * uniform + alpha * empirical[code]
    return normalize(prior)

# ─────────────────────────────────────────────────────
# 推奨指標(事前重み付き情報利得 + 微小ヒットバイアス + 条件付きボーナス)

def bucket_entropy_with_prior(cands: Sequence[str], guess: str, prior: Dict[str, float]) -> float:
    buckets: Dict[Tuple[int, int], float] = defaultdict(float)
    for s in cands:
        r = hb(s, guess)
        buckets[r] += prior.get(s, 0.0)
    probs = [v for v in buckets.values() if v > 0.0]
    if not probs:
        return 0.0
    return -sum(p * math.log(p, 2) for p in probs)  # log2

def recommend_from(
    cands: Sequence[str],
    prior: Dict[str, float],
    conditional_bonus: Dict[str, int] | None = None,
    bonus_lambda: float = SECOND_MOVE_BONUS_LAMBDA,
) -> str:
    if not cands:
        raise ValueError("候補集合が空です(直前の入力に矛盾がある可能性)。")

    scores: List[Tuple[str, float]] = []
    for g in cands:
        hR = bucket_entropy_with_prior(cands, g, prior)
        hit_term = math.log2(max(prior.get(g, 0.0), SMOOTHING)) * HIT_BIAS  # log2
        score = hR + hit_term
        if conditional_bonus and g in conditional_bonus:
            score += bonus_lambda * math.log2(1.0 + conditional_bonus[g])   # log2
        scores.append((g, score))

    if TEMPERATURE <= 0:
        maxv = max(v for _, v in scores)
        pool = [g for g, v in scores if abs(v - maxv) < 1e-12]
        return random.choice(pool)

    mx = max(v for _, v in scores)
    exps = [math.exp((v - mx) / TEMPERATURE) for _, v in scores]
    z = sum(exps)
    r = random.random() * z
    acc = 0.0
    for (g, _), e in zip(scores, exps):
        acc += e
        if acc >= r:
            return g
    return scores[-1][0]

# ─────────────────────────────────────────────────────
# 例外型

class HBContradiction(Exception):
    """矛盾(候補∅)を上位に伝えるための内部例外。"""
    pass

# ─────────────────────────────────────────────────────
# セッション管理

class HBSession:
    def __init__(self) -> None:
        if RNG_SEED is not None:
            random.seed(RNG_SEED)
        self.universe: List[str] = all_codes()
        self.candidates: List[str] = list(self.universe)
        self.prior: Dict[str, float] = build_initial_prior(self.universe)
        self.history: List[Tuple[str, int, int]] = []  # (guess, H, B)
        self.first_move = True
        self._stack: List[Tuple[List[str], Dict[str, float], List[Tuple[str, int, int]], bool]] = []

    def snapshot(self):
        return (list(self.candidates), dict(self.prior), list(self.history), self.first_move)

    def restore(self, snap) -> None:
        self.candidates, self.prior, self.history, self.first_move = snap

    def reset(self) -> None:
        """初期状態へ完全リセット。"""
        self.universe = all_codes()
        self.candidates = list(self.universe)
        self.prior = build_initial_prior(self.universe)
        self.history = []
        self.first_move = True
        self._stack = []

    def parse_entry(self, entry: str) -> Tuple[str, int, int]:
        if ":" not in entry:
            raise ValueError("入力は 'XYZ:HB' 形式。例 198:10")
        guess_raw, fb_raw = entry.split(":", 1)
        guess = guess_raw.strip()
        fb = fb_raw.strip()
        if len(guess) != CODE_LENGTH or not guess.isdigit():
            raise ValueError(f"推測は {CODE_LENGTH} 桁の数字")
        if not ALLOW_DUPLICATES and len(set(guess)) != CODE_LENGTH:
            raise ValueError("各桁は相異(重複禁止)")
        if len(fb) != 2 or not fb.isdigit():
            raise ValueError("応答は 2桁 'HB'(例 10 は 1H0B)")  # ← 修正
        h, b = int(fb[0]), int(fb[1])
        if not (0 <= h <= CODE_LENGTH and 0 <= b <= CODE_LENGTH and h + b <= CODE_LENGTH):
            raise ValueError("H/B が無効")
        return guess, h, b

    def step(self, entry: str) -> Tuple[int, str | None]:
        guess, h, b = self.parse_entry(entry)
        snap = self.snapshot()

        # 事後更新:整合コードのみ質量を保持
        post: Dict[str, float] = {}
        for s, p in self.prior.items():
            post[s] = p if hb(s, guess) == (h, b) else 0.0
        post = normalize(post)

        # 候補集合の刈り込み
        new_cands = [s for s in self.candidates if hb(s, guess) == (h, b)]
        if not new_cands:
            if CONTRADICTION_BEHAVIOR == 'undo':
                self.restore(snap)
                raise ValueError("矛盾:候補が空になりました。直前状態へ戻しました。入力をご確認ください。")
            else:
                raise HBContradiction("矛盾:候補が空になりました。セッションを初期化します。入力をご確認ください。")

        # 正常適用
        self._stack.append(snap)
        self.prior = post
        self.candidates = new_cands
        self.history.append((guess, h, b))
        self.first_move = False

        if len(self.candidates) == 1:
            return 1, "確定"
        return len(self.candidates), None

    def conditional_bonus_for_next_move(self) -> Dict[str, int] | None:
        """
        実測「2回目の034が頻出」の反映:
        ・履歴が 1 手目の直後で、
        ・その “直前入力のコード” がトリガ集合 {198, 578, 785, 876} に含まれる
          —— あるいは “現在の MAP(事後最大)” がトリガ集合に含まれる —— 場合、
          次手で '034' を強く優先(候補整合時のみ有効)。
        """
        if len(self.history) != 1:
            return None
        last_guess = self.history[0][0]
        if last_guess in SECOND_MOVE_TRIGGER_SEEDS:
            return SECOND_MOVE_TARGET_BONUS
        # 代替条件:現事後の最尤コードがトリガ集合に属する場合にも適用
        top_code = max(self.prior.items(), key=lambda kv: kv[1])[0] if self.prior else None
        if top_code in SECOND_MOVE_TRIGGER_SEEDS:
            return SECOND_MOVE_TARGET_BONUS
        return None

    def recommend(self) -> Tuple[str, float]:
        bonus = self.conditional_bonus_for_next_move()
        g = recommend_from(self.candidates, self.prior, conditional_bonus=bonus)
        p_hit = self.prior.get(g, 0.0)
        return g, p_hit

# ─────────────────────────────────────────────────────
# 入出力部分

def main() -> None:
    print("FC版女神転生II特化型コードブレイカー・ソルバー")
    print("入力形式:XYZ:HB  例)198:10 は 1Hit 0Blow/コマンド:prior/reset/undo/quit\n")

    sess = HBSession()

    while True:
        try:
            if not sess.candidates:
                print("候補集合が空です。'reset' で初期化するか、'undo' で戻してください。")
            else:
                rec, p = sess.recommend()
                pct = 100.0 * p
                if pct >= 99.95 and len(sess.candidates) > 1:
                    pct = 99.9
                print(f"推奨: {rec}  (直撃確率 ≈ {pct:.1f}%|候補 {len(sess.candidates)}")
            line = input(f"{len(sess.history)+1}> ").strip()
            if not line:
                continue
            cmd = line.lower()
            if cmd in ("quit", "q", "exit"):
                print("終了します。")
                break
            if cmd == "prior":
                top = sorted(sess.prior.items(), key=lambda x: -x[1])[:10]
                s = ", ".join(f"{k}:{v*100:.1f}%" for k, v in top)
                print(f"現在の事後上位: {s}")
                continue
            if cmd == "reset":
                sess = HBSession()
                print("新セッションを開始(初期化)。")
                continue
            if cmd == "undo":
                if sess.undo():
                    print("直前の入力を取り消しました。")
                else:
                    print("取り消し可能な履歴がありません。")
                continue

            # 通常の推移
            n, status = sess.step(line)
            if status:
                print(f"{status}: 正解 → {sess.candidates[0]}")
                break

        except HBContradiction as e:
            print(f"{e}\n(初期状態にリセットします)")
            sess = HBSession()
            continue
        except Exception as e:
            print(f"エラー: {e}")
            continue

if __name__ == "__main__":
    main()

以上です。お役に立てれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?