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?

女神転生II(FC版)のコードブレイカー(3桁の数字を当てるやつ)を頑張って当てるためのコード

Last updated at Posted at 2025-08-16

はじめの前のおねがい

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

はじめに

このコードはPython 3Pythonista 3に対応しています。

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

本コードは女神転生II(FC版)のコードブレイカー(3桁の数字を当てるやつ)を頑張って当てるためのコードです。

実行すると入力画面になるので

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

例えば“019:12”ならば入力した数字が109で、1ヒット2ブローと返事が来たということになる。

コードブレイカーって何?

Googleで女神転生II コードブレイカーで適当に検索して調べて下さい。あっ、ChatGPT-5に聞いたら4桁の数字と答えていましたが(2025-08-17時点)、女神転生IIのコードブレイカーは3桁の数字です。

……という書き方も無責任だと言われそうなので、簡単に説明すると、

  1. 相手はそれぞれ0から9まで、3桁の重複しない数字を紙などに記入(111や121など重複はNG)
  2. 自分はその数字を当てる
  3. 相手は言われた数字を元に「順番と数字が当たっていた個数を『◯ヒット』、数字のみが当たっていた個数を『◯ブロー』」と返事しなければならない

というルールです。そうすると確率的には720通りになるために、1回で当たるのは約0.37パーセントくらいになるのですが、「◯ヒット◯ブロー」と返事をしてもらえるので、それを元に数字を絞り込んでいくことができます。

ちなみに上述のChatGPTの回答のような、4桁版ももちろん存在しますが、当然確率が低くなるので、回数制限を設ける場合、3桁がちょこっと楽しむには、ちょうどいい感じです。

こういうのが得意な人ならば4〜5回くらいで当てることができるのですが、私のような生まれつき運が悪い人のために、本コードを作成しました。

ちなみに対コンピューター戦のように純粋に乱数で3桁の数字を作成している場合は、012, 345と提示していく方法が有効なのですが、対人戦の場合だとそのバターンを読んで、自動的に987など数を大きくすることで少ない回数で当てられる確率を低くすることができます。そのために本コードでは数字に乱数性を持たせたパターンのものを用意してあります。

謝辞

本コードはpy 乙様のプログラミング上達講座3:メガテンのコードブレイカーの記事を大きく参考にさせていただきました。ここにお礼を申し上げます。

Pythonの必要なモジュール

ありません

ソースコード

簡易版と候補数が複数あるガチ勢版を用意しました。何度か試してみて、個人的には簡易版で十分なんじゃないかな、と思います。

簡易版

下記は「とりあえず手軽に実行したい人」向けの簡易版です。デフォルトで数字に乱数性を持たせてありますが、コンピューターの場合、初手を012にし続ければ、当然、毎回乱数にするよりも当たる確率が高くなるかと思います。最初の推測はあくまで提案に過ぎないわけですから、ここで012としてヒット数ブロー数を入力しても、もちろん何ら問題はありません。

quitで途中終了することができ、undoで1回戻ることができます。

Hit_and_Blow.py
# 3桁 Hit & Blow(3桁の数値がそれぞれ異なる場合)
# ───────────────────────────────
# 【用語】
# H → Hit = ヒット、数字と数字の位置が当たっている場合
# B → Blow = ブロー、数字の位置のみが当たっている場合
# ───────────────────────────────
# 【操作説明】
# 1) 毎手、「最初の推測:」「次の推測:」に推奨手を表示。
#    「当たり確率」を整数の百分率で表示。
#    例:候補数 |S| は 720 。
# 2) 入力形式: xyz:mn(例 012:10)
# 3) 特別コマンド:
#    “undo” / 取り消し → 直前に戻る
#    “終了” / “quit” / “exit” / “q” → 終了
# 4) 終了条件:
#    XYZ:30 → 3ヒット0ブローの場合 → その数字を含めた候補集合を列挙
#    候補が1つだけになった場合 → その数字を表示
# ───────────────────────────────
# 【パラメータ(0〜10整数指定、内部で実数変換)】
# 基本的には変更する必要なし
RNG_SEED          = None
TEMPERATURE_LEVEL = 2   # ランダム度合い(0〜10 → /10.0)
EPSILON_LEVEL     = 1   # 近傍幅(0〜10 → /100.0)

TEMPERATURE = TEMPERATURE_LEVEL / 10.0
EPSILON     = EPSILON_LEVEL / 100.0
# ───────────────────────────────

from itertools import permutations
from collections import defaultdict
import math, random

def all_codes():
    return [''.join(p) for p in permutations('0123456789', 3)]

UNIVERSE = all_codes()

def hb(secret, guess):
    H = sum(1 for i in range(3) if secret[i] == guess[i])
    B = sum(1 for ch in guess if ch in secret) - H
    return (H, B)

def partition_sizes(cands, g):
    buckets = defaultdict(int)
    for s in cands:
        buckets[hb(s, g)] += 1
    return buckets

def metrics_on(cands, g):
    n = len(cands)
    if n == 0: return {'worst':0,'exp_rem':0.0,'entropy':0.0}
    buckets = partition_sizes(cands,g)
    worst = max(buckets.values()) if buckets else 0
    exp_rem = sum(b*b for b in buckets.values())/n
    Hbits = 0.0
    for b in buckets.values():
        p = b/n
        Hbits -= p*math.log(p,2)
    return {'worst':worst,'exp_rem':exp_rem,'entropy':Hbits}

def softmax_choice(items, score_fn, temperature):
    vals = [score_fn(x) for x in items]
    if temperature <= 0:
        maxv = max(vals)
        pool = [it for it,v in zip(items,vals) if v==maxv]
        return random.choice(pool)
    mx = max(vals)
    exps = [math.exp((v-mx)/temperature) for v in vals]
    z = sum(exps)
    r = random.random()*z
    acc = 0.0
    for it,e in zip(items,exps):
        acc += e
        if acc >= r:
            return it
    return items[-1]

def hybrid_select_in_S(cands):
    stats = {g:metrics_on(cands,g) for g in cands}
    min_exp = min(stats[g]['exp_rem'] for g in cands)
    near = [g for g in cands if stats[g]['exp_rem'] <= (1.0+EPSILON)*min_exp+1e-12]
    picked = softmax_choice(near, lambda g: stats[g]['entropy'], TEMPERATURE)
    return picked

def filter_candidates(cands, guess,H,B):
    return [s for s in cands if hb(s,guess)==(H,B)]

# 真正の四捨五入(round-half-up)
def round_half_up(x: float) -> int:
    return int(math.floor(x + 0.5))

class HBSession:
    def __init__(self):
        if RNG_SEED is not None: random.seed(RNG_SEED)
        self.candidates = UNIVERSE.copy()
        self.prev_candidates = UNIVERSE.copy()
        self.history = []
        self.first_move = True
        self.stack = []
        
    def parse_entry(self, entry):
        if ':' not in entry: raise ValueError("入力は 'xyz:mn' 形式。")
        guess, fb = entry.split(':',1)
        guess, fb = guess.strip(), fb.strip()
        if len(guess)!=3 or not guess.isdigit(): raise ValueError("推測は3桁数字。")
        if len(set(guess))!=3: raise ValueError("桁は相異でなければならない。")
        if len(fb)!=2 or not fb.isdigit(): raise ValueError("応答は2桁 'mn'")
        H,B=int(fb[0]),int(fb[1])
        if not(0<=H<=3 and 0<=B<=3 and H+B<=3): raise ValueError("無効なH/B。")
        return guess,H,B
    
    def update(self, entry):
        self.stack.append((list(self.candidates),list(self.history),list(self.prev_candidates),self.first_move))
        self.prev_candidates = list(self.candidates)
        guess,H,B=self.parse_entry(entry)
        if H==3 and B==0:
            self.history.append((guess,H,B))
            self.candidates=[guess]
            return 1,'solved_by_3h0b'
        self.candidates=filter_candidates(self.candidates,guess,H,B)
        self.history.append((guess,H,B))
        if len(self.candidates)==1:
            return 1,'solved_by_unique'
        return len(self.candidates),None
    
    def undo(self):
        if not self.stack: return False
        c,h,p,f=self.stack.pop()
        self.candidates,self.history,self.prev_candidates,self.first_move=c,h,p,f
        return True
    
    def recommend(self):
        return hybrid_select_in_S(self.candidates)
    
if __name__=='__main__':
    if RNG_SEED is not None: random.seed(RNG_SEED)
    s=HBSession()
    print("ヒット&ブロー(3桁・各桁相異)— 当たり確率「約」表示・自動巻き戻し版(v16-jp-prob-approx-auto-undo)")
    print("「undo」または「取り消し」で直前の手に戻ります。「終了/quit」で終了します。\n")
    
    while True:
        # S=∅ を自動的に救済して継続可能にする
        if not s.candidates:
            # 自動で直前入力を取り消し
            if s.undo():
                print("候補が存在しません。直前の入力を取り消して続行します。もう一度入力してください。\n")
                # 取り消し後にループの先頭へ戻り、推奨手を再提示
                continue
            else:
                # 取り消せない(理論上まれ)場合は安全のため初期状態へ復帰
                print("候補が存在しません。初期状態にリセットします。")
                s.candidates = UNIVERSE.copy()
                s.prev_candidates = UNIVERSE.copy()
                s.history.clear()
                s.first_move = True
                continue
            
        rec=s.recommend()
        n = len(s.candidates)
        p_exact = 100.0 / n
        p_pct   = round_half_up(p_exact)
        approx  = (abs(p_exact - p_pct) > 1e-12)
        
        label='最初の推測' if s.first_move else '次の推測'
        s.first_move=False
        if approx:
            print(f"{label}: {rec} (当たり確率 約{p_pct}%)")
        else:
            print(f"{label}: {rec} (当たり確率 {p_pct}%)")
            
        print(f"残り候補数: {n}")
        print("XYZ:HB")
        line=input("").strip()
        
        if line in ('終了','quit','exit','q','Q'):
            print("終了します。"); break
            
        if line.lower()=='undo' or line=='取り消し':
            if s.undo():
                print(f"取り消しました。候補数: {len(s.candidates)}\n")
            else:
                print("取り消せません。\n")
            continue
        
        try:
            rem,status=s.update(line)
            if status=='solved_by_3h0b':
                print("確定")
                print(' '.join(sorted(s.prev_candidates)))
                break
            if status=='solved_by_unique':
                print("確定")
                print(' '.join(sorted(s.candidates)))
                break
        except Exception as e:
            print(f"エラー: {e}。再入力してください。\n")
            continue

ガチ勢版

結局、どの数字かを決めるかは自分なのですが、様々な候補を考えたい人向けです。

Hit_and_Blow_en.py (英語版)
# 3-digit hit & blow (no repeats) — hybrid (exp+entropy) multi-policy (English UI)
# ───────────────────────────────────────────────────────────
# 【概要】
# - 問題設定:桁集合 {0,…,9} から相異なる3桁を選んだ秘密コード s を、Hit & Blow(3桁・重複禁止)で同定する。
# - 本プログラムは、毎手「推奨手」を複数方策で**同時提示**し、入力された応答 (H,B) に基づき候補集合 S を刈り込む。
# - 方策:
#     (1) candidate-minimax        …… 現候補 S から選ぶミニマックス(最大残余候補数を最小化)
#     (2) unrestricted-minimax     …… 全宇宙 U から選ぶミニマックス(評価は S 上の分割)
#     (3) entropy (info-gain)      …… U から選び、S 上のエントロピー(情報量)を最大化
#     (4) hybrid (exp+entropy, stochastic)
#         ① 期待残余候補数(exp_remaining)を最小化
#         ② その相対近傍(ε)に限定
#         ③ 近傍内でエントロピー最大化
#         ④ ソフトマックス(確率的タイブレーク)で一点選抜
# - 終了仕様:
#     • 3H0B を受けたときは「その**入力適用直前**の候補集合 S を列挙」し、"completed" を出力して終了。
#     • 刈り込み後に |S|=1(論理的一意化)なら、その唯一要素を列挙して "completed"。
# - 取り消し機能:入力ミスに備え "undo" で直前状態へ復元。
#
# ───────────────────────────────────────────────────────────
# 【操作説明(実行時の使い方)】
# 1) 実行すると、毎手ごとに推奨手(4行:candidate-minimax / unrestricted-minimax / entropy / hybrid)が表示される。
#    • "first guess:"(初手)→ 以後は "next guess:"。
#    • 各行の角括弧内は評価指標:
#        - max_remaining       : その手を打ったときに、(H,B) 応答の中で最大クラスに残る候補数(最悪ケースの残数)。
#        - expected_entropy    : S 上のエントロピー(情報量)の期待値(bit)。
#        - exp_remaining       : 「期待残余候補数」= Σ b_i^2 / |S|(b_i は (H,B) 各クラスの濃度)。
#        - in_solution_set     : その推測が現候補 S に属する(=解答候補)か、outside_solution_set(=候補外の“試験的推測”)か。
# 2) 入力形式は常に "xyz:mn"(例 "012:10")。見出し "XYZ:HB" の次のプロンプト ">" に入力する。
#    • xyz:相異なる3桁の数字。例 012, 057, 780(777 のような重複は不可)。
#    • mn :H と B を連ねた2桁の数字。例 "10" は 1H1B、"30" は 3H0B(正解)。
# 3) 特別コマンド:
#    • "undo" …… 直前の状態(候補集合 S・履歴・初手フラグなど)に戻る。
#    • "quit" / "exit" / "q" …… 終了。
# 4) 終了時の表示:
#    • 3H0B のとき:直前に残っていた候補を**空白区切り**で全列挙(検証・対人戦の分析に有用)。
#    • 一意化のとき:唯一の候補を列挙。
#
# ───────────────────────────────────────────────────────────
# 【パラメーター設計(“0〜10 の整数スケール”で設定し、内部で実数に変換)】
# - TEMPERATURE_LEVEL ∈ {0,…,10} を乱択度合いの調整ノブとして採用し、内部で TEMPERATURE = LEVEL / 10.0 に写像。
#     直感:0 → 決定論的(最大値同士は一様乱択のみ)/10 → 乱択強め(softmax がほぼ一様化)
# - EPSILON_LEVEL ∈ {0,…,10} を“最良近傍の相対幅(%)”として採用し、内部で EPSILON = LEVEL / 100.0 に写像。
#     直感:0 → 最良値のみ採用/10 → 最良値 +10% までを“ほぼ同等”として許容
# - RNG_SEED は乱数の再現性制御。None なら実行毎に異なる挙動、整数なら結果が再現可能。
#
# 【デフォルトと調整の指針】
# - 既定:TEMPERATURE_LEVEL=2(控えめな乱択)、EPSILON_LEVEL=1(+1% 近傍)。
#   → 解探索の安定性と“読まれにくさ”のバランスが良い。
# - 対人戦で読みを外したい:TEMPERATURE_LEVEL を 3〜4 へ上げる(多様化)/散漫なら 1 へ下げる。
# - 近傍幅の微調整:まず 0〜2 の範囲で。0 は厳格最良のみ、2 は +2% まで許容。
#
# ───────────────────────────────────────────────────────────
# User parameters (0–10 integer scale; converted internally)
RNG_SEED          = None  # None: run-to-run variability / int: reproducible randomness(例: 12345)
TEMPERATURE_LEVEL = 2     # 0–10(乱択度合い):内部で TEMPERATURE = LEVEL / 10.0
EPSILON_LEVEL     = 1     # 0–10(最良相対近傍%):内部で EPSILON = LEVEL / 100.0

# Internal conversion(アルゴリズムが用いる実数値)
TEMPERATURE = TEMPERATURE_LEVEL / 10.0  # 例: 2→0.2
EPSILON     = EPSILON_LEVEL / 100.0     # 例: 1→0.01
# ───────────────────────────────────────────────────────────

from itertools import permutations
from collections import defaultdict
import math, random

# U:許容コード全体(10P3 = 720)。各桁は相異(重複なし)。
def all_codes():
    return [''.join(p) for p in permutations('0123456789', 3)]

UNIVERSE = all_codes()

# 応答写像 hb : (secret, guess) ↦ (H, B)
# H:座位一致数、B:値一致数から H を控除した個数(= “位置違い”の一致数)。
def hb(secret: str, guess: str):
    H = sum(1 for i in range(3) if secret[i] == guess[i])
    B = sum(1 for ch in guess if ch in secret) - H
    return (H, B)

# partition_sizes:現候補 S を hb(·, g) によって (H,B) ごとに分割し、各同値類の濃度を返す。
def partition_sizes(cands, g: str):
    buckets = defaultdict(int)
    for s in cands:
        buckets[hb(s, g)] += 1
    return buckets  # 例:{(1,0): 12, (0,2): 8, ...}

# metrics_on:指標の一括計算。
# - worst(最大残余候補数)= max_i b_i
# - exp_rem(期待残余候補数)= Σ_i b_i^2 / |S|
# - entropy(エントロピー)= -Σ_i (b_i/|S|) log2(b_i/|S|)
def metrics_on(cands, g: str):
    n = len(cands)
    if n == 0:
        return {'worst': 0, 'exp_rem': 0.0, 'entropy': 0.0, 'buckets': {}}
    buckets = partition_sizes(cands, g)
    worst = max(buckets.values()) if buckets else 0
    exp_rem = sum(b*b for b in buckets.values()) / n
    Hbits = 0.0
    for b in buckets.values():
        p = b / n
        Hbits -= p * math.log(p, 2)
    return {'worst': worst, 'exp_rem': exp_rem, 'entropy': Hbits, 'buckets': buckets}

# softmax_choice:温度 T のソフトマックスで items から 1 点を確率的に選ぶ。
#   score_fn は「大きいほど良い」スコアを返す関数。
def softmax_choice(items, score_fn, temperature):
    vals = [score_fn(x) for x in items]
    if temperature <= 0:
        # 極限:最大値同士の一様乱択(決定論的な近似)
        maxv = max(vals)
        pool = [it for it, v in zip(items, vals) if v == maxv]
        return random.choice(pool)
    mx = max(vals)
    exps = [math.exp((v - mx) / temperature) for v in vals]
    z = sum(exps)
    r = random.random() * z
    acc = 0.0
    for it, e in zip(items, exps):
        acc += e
        if acc >= r:
            return it
    return items[-1]

# hybrid_select(本版の要):期待残数→近傍→エントロピー→乱択 の順で選抜する。
# 直感:
#   ① Σ b_i^2 / |S|(“次手の期待的な取り残し”)を最小化。
#   ② “ほぼ最良”集合(相対誤差 ≤ EPSILON)に限定。
#   ③ 近傍内でエントロピー(情報量)を指標に優先順位付け。
#   ④ 最後に softmax で多様化(対称性と対人耐性を確保)。
def hybrid_select(cands, G):
    stats = {g: metrics_on(cands, g) for g in G}
    min_exp = min(stats[g]['exp_rem'] for g in G)
    # “ほぼ最良”の相対近傍:min_exp * (1 + EPSILON) 以下。
    near = [g for g in G if stats[g]['exp_rem'] <= (1.0 + EPSILON) * min_exp + 1e-12]
    # 近傍内ではエントロピーをスコアとして softmax 乱択。
    picked = softmax_choice(near, lambda g: stats[g]['entropy'], TEMPERATURE)
    s = stats[picked]
    return picked, s['worst'], s['exp_rem'], s['entropy']

# 参考:最悪分割(最大同値類濃度)を最小化するミニマックス。
def minimax_over(cands, G):
    best_g, best_worst, best_is_candidate = None, float('inf'), False
    for g in G:
        w = metrics_on(cands, g)['worst']
        is_cand = (g in cands)
        # 同点時は(i)候補内優先、(ii)語順最小で裁く。
        key = (w, 0 if is_cand else 1, g)
        if best_g is None or key < (best_worst, 0 if best_is_candidate else 1, best_g):
            best_g, best_worst, best_is_candidate = g, w, is_cand
    return best_g, best_worst, best_is_candidate

# 参考:情報量(エントロピー)最大化。
def entropy_choice(cands, G):
    best_g, best_Hbits, best_is_candidate = None, -1.0, False
    for g in G:
        Hbits = metrics_on(cands, g)['entropy']
        is_cand = (g in cands)
        key = (-Hbits, 0 if is_cand else 1, g)
        if best_g is None or key < (-best_Hbits, 0 if best_is_candidate else 1, best_g):
            best_g, best_Hbits, best_is_candidate = g, Hbits, is_cand
    return best_g, best_Hbits, best_is_candidate

# 刈り込み作用素:S ← { s ∈ S | hb(s, guess) = (H, B) }。
def filter_candidates(cands, guess: str, H: int, B: int):
    return [s for s in cands if hb(s, guess) == (H, B)]

class HBSession:
    def __init__(self):
        # 乱数シード(None なら run-to-run で変動)
        if RNG_SEED is not None:
            random.seed(RNG_SEED)
        self.candidates = UNIVERSE.copy()       # 現候補集合 S
        self.prev_candidates = UNIVERSE.copy()  # 入力適用“直前”の S(3H0B 表示用)
        self.history = []                       # 履歴列 [(guess, H, B), ...]
        self.first_move = True                  # 初回表示フラグ("first guess" 用)
        self.stack = []                         # undo 用:適用前状態のスタック

    # 入力検証:'xyz:mn' 形式・桁相異・値域制約(違反時は例外)
    def parse_entry(self, entry: str):
        entry = entry.strip()
        if ':' not in entry:
            raise ValueError("input must be 'xyz:mn' (colon missing).")
        guess, fb = entry.split(':', 1)
        guess, fb = guess.strip(), fb.strip()
        if len(guess) != 3 or not guess.isdigit():
            raise ValueError("guess must be exactly three digits (e.g., '012').")
        if len(set(guess)) != 3:
            raise ValueError("digits must be all distinct (no repeats).")
        if len(fb) != 2 or not fb.isdigit():
            raise ValueError("feedback must be two digits 'mn' (e.g., '10' for 1H1B).")
        H, B = int(fb[0]), int(fb[1])
        if not (0 <= H <= 3 and 0 <= B <= 3 and H + B <= 3):
            raise ValueError("invalid H/B (require 0 ≤ H,B ≤ 3 and H+B ≤ 3).")
        return guess, H, B

    # 状態遷移:'xyz:mn' を適用。返値は (|S|, status)、status ∈ {None, 'solved_by_3h0b', 'solved_by_unique'}。
    def update(self, entry: str):
        # undo 用に適用前の状態を保存
        self.stack.append((
            list(self.candidates),
            list(self.history),
            list(self.prev_candidates),
            self.first_move
        ))
        # 3H0B 時に“直前 S”を表示するため控える
        self.prev_candidates = list(self.candidates)

        guess, H, B = self.parse_entry(entry)

        if H == 3 and B == 0:
            # 全一致。内部的には candidates を一意化するが、終了時の表示は prev_candidates を用いる。
            self.history.append((guess, H, B))
            self.candidates = [guess]
            return 1, 'solved_by_3h0b'

        # 通常の刈り込み
        self.candidates = filter_candidates(self.candidates, guess, H, B)
        self.history.append((guess, H, B))

        if len(self.candidates) == 1:
            return 1, 'solved_by_unique'
        return len(self.candidates), None

    # 1手巻き戻し(成功 True/不可 False)
    def undo(self):
        if not self.stack:
            return False
        cands, hist, prev, first = self.stack.pop()
        self.candidates, self.history = cands, hist
        self.prev_candidates, self.first_move = prev, first
        return True

    # 推奨手(従来 3 方策+ハイブリッド 2 種)を同時計算して返す。
    def recommend_all(self):
        S, U = self.candidates, UNIVERSE

        # 参考方策
        g1, w1, c1 = minimax_over(S, S)   # candidate-minimax(G=S)
        g2, w2, c2 = minimax_over(S, U)   # unrestricted-minimax(G=U、評価は S)
        g3, H3, c3 = entropy_choice(S, U) # entropy

        # ハイブリッド(G=S と G=U)
        ghS, wS, eS, hS = hybrid_select(S, S)
        ghU, wU, eU, hU = hybrid_select(S, U)

        return {
            'candidate_minimax':    {'guess': g1,  'max_remaining': w1, 'in_candidates': c1},
            'unrestricted_minimax': {'guess': g2,  'max_remaining': w2, 'in_candidates': c2},
            'entropy':              {'guess': g3,  'entropy_bits': H3,  'in_candidates': c3},
            'hybrid_in_S':          {'guess': ghS, 'max_remaining': wS, 'exp_remaining': eS, 'entropy_bits': hS, 'in_candidates': True},
            'hybrid_in_U':          {'guess': ghU, 'max_remaining': wU, 'exp_remaining': eU, 'entropy_bits': hU, 'in_candidates': (ghU in S)},
        }

# 集合関係の表示ラベル
def in_set_label(flag: bool) -> str:
    return 'in_solution_set' if flag else 'outside_solution_set'

if __name__ == '__main__':
    # RNG_SEED は HBSession.__init__ でも適用しているが、冪等のためここでも明示
    if RNG_SEED is not None:
        random.seed(RNG_SEED)

    s = HBSession()
    print('hit & blow (3-digit, no repeats) — hybrid (exp+entropy) multi-policy with undo (v11c)')
    print("type 'undo' to revert the last step; type 'quit' to exit.\n")

    while True:
        # 矛盾的入力の累積などにより S=∅ になった場合は終了(前段の入力列に不整合がある可能性)
        if not s.candidates:
            print('no feasible candidates remain — please check previous inputs.')
            break

        rec = s.recommend_all()
        label = 'first guess' if s.first_move else 'next guess'
        s.first_move = False

        # 推奨手の提示(評価語は “max_remaining / expected_entropy / exp_remaining / in_solution_set …” に統一)
        print(f'{label}:')
        print(f"  candidate-minimax    : {rec['candidate_minimax']['guess']}  "
              f"[max_remaining={rec['candidate_minimax']['max_remaining']}; {in_set_label(rec['candidate_minimax']['in_candidates'])}]")
        print(f"  unrestricted-minimax : {rec['unrestricted_minimax']['guess']}  "
              f"[max_remaining={rec['unrestricted_minimax']['max_remaining']}; {in_set_label(rec['unrestricted_minimax']['in_candidates'])}]")
        print(f"  entropy (info-gain)  : {rec['entropy']['guess']}  "
              f"[expected_entropy={rec['entropy']['entropy_bits']:.4f} bits; {in_set_label(rec['entropy']['in_candidates'])}]")
        print(f"  hybrid (in S)        : {rec['hybrid_in_S']['guess']}  "
              f"[exp_remaining={rec['hybrid_in_S']['exp_remaining']:.2f}; entropy={rec['hybrid_in_S']['entropy_bits']:.4f} bits; "
              f"max_remaining={rec['hybrid_in_S']['max_remaining']}; in_solution_set]")
        print(f"  hybrid (in U)        : {rec['hybrid_in_U']['guess']}  "
              f"[exp_remaining={rec['hybrid_in_U']['exp_remaining']:.2f}; entropy={rec['hybrid_in_U']['entropy_bits']:.4f} bits; "
              f"max_remaining={rec['hybrid_in_U']['max_remaining']}; {in_set_label(rec['hybrid_in_U']['in_candidates'])}]")
        print(f'  remaining candidates : {len(s.candidates)}')

        # 入力インターフェース(“XYZ:HB” 見出しは仕様)
        print('XYZ:HB')
        line = input('> ').strip()

        # 終了コマンド
        if line.upper() in ('Q', 'QUIT', 'EXIT'):
            print('bye.')
            break

        # undo(直前状態へ巻き戻し)
        if line.lower() == 'undo':
            if s.undo():
                print(f'undone. now {len(s.candidates)} candidates remain.\n')
            else:
                print('cannot undo. already at initial state.\n')
            continue

        # 通常の状態更新
        try:
            rem, status = s.update(line)

            # 3H0B による確定:直前の S を列挙
            if status == 'solved_by_3h0b':
                print('completed')
                print(' '.join(sorted(s.prev_candidates)))
                break

            # 一意化による確定:現 S(1 元集合)を列挙
            if status == 'solved_by_unique':
                print('completed')
                print(' '.join(sorted(s.candidates)))
                break

        except Exception as e:
            # 入力形式・値域違反・桁重複などはここで通知し、ループを継続
            print(f'error: {e} please try again.\n')
            continue
Hit_and_Blow_jp.py (日本語版)
# 3桁 Hit & Blow(各桁相異)— hybrid(期待値+情報量)複数方策アシスタント(日本語UI)
# ───────────────────────────────────────────────────────────
# 【概要】
# - 問題設定:桁集合 {0,…,9} から相異なる3桁を選んだ秘密コード s を、Hit & Blow(3桁・重複禁止)で同定する。
# - 本プログラムは、毎手「推奨手」を複数方策で**同時提示**し、入力された応答 (H,B) に基づき候補集合 S を刈り込む。
# - 方策:
#     (1) 候補制限ミニマックス(candidate-minimax)
#         …… 現候補 S のみを許容推測集合とし、(H,B) による分割の最大同値類(最大残余候補数)を最小化。
#     (2) 全体ミニマックス(unrestricted-minimax)
#         …… 全宇宙 U を許容推測集合とし、評価は S 上の分割に対して同様に行う。
#     (3) エントロピー最大化(entropy / information gain)
#         …… U から選び、S 上の (H,B) 分布のシャノンエントロピー(bit)を最大化。
#     (4) ハイブリッド(hybrid:exp+entropy, stochastic)
#         ① 期待残余候補数(exp_remaining)を最小化
#         ② その相対近傍(ε)に限定
#         ③ 近傍内でエントロピー最大化
#         ④ ソフトマックス(確率的タイブレーク)で一点選抜
# - 終了仕様:
#     • 応答が 3H0B のとき:その入力適用直前の候補集合 S を空白区切りで列挙し、「確定」と表示して終了。
#     • 刈り込み後に |S|=1(論理的一意化):その唯一要素を列挙し、「確定」と表示して終了。
# - 取り消し機能:入力ミスに備え、「undo」または「取り消し」で直前状態へ復元。
#
# ───────────────────────────────────────────────────────────
# 【操作説明(実行時の使い方)】
# 1) 実行すると、毎手ごとに推奨手(4行:候補制限ミニマックス/全体ミニマックス/エントロピー最大化/ハイブリッド)が表示される。
#    • 初手は「最初の推測:」、以後は「次の推測:」。
#    • 各行の角括弧内は評価指標:
#        - 最大残余候補数(max_remaining): その手の応答 (H,B) のうち最大クラスに残る候補数(最悪ケースの残数)。
#        - エントロピー期待値(expected_entropy): S 上の情報量(bit)。
#        - 期待残余候補数(exp_remaining): Σ b_i^2 / |S|(b_i は (H,B) 各クラスの濃度)。
#        - 解答候補に含まれる/含まれない: 推測が現候補 S に属するか否か(外部プローブの明示)。
# 2) 入力形式は常に「xyz:mn」(例「012:10」)。見出し「XYZ:HB」に続くプロンプト「>」に入力する。
#    • xyz:相異なる3桁の数字(例 012, 057, 780。777 のような重複は不可)。
#    • mn :H と B を連ねた2桁の数字(例「10」は 1H1B、「30」は 3H0B=正解)。
# 3) 特別コマンド:
#    • 「undo」または「取り消し」…… 直前の状態(候補集合 S・履歴・初手フラグなど)に戻る。
#    • 「終了」/「quit」/「exit」/「q」…… 終了。
# 4) 終了時の表示:
#    • 3H0B のとき:直前に残っていた候補を**空白区切り**で全列挙(検証・対人戦の分析に有用)。
#    • 一意化のとき:唯一の候補を列挙。
#
# ───────────────────────────────────────────────────────────
# 【パラメーター設計(“0〜10 の整数スケール”で設定し、内部で実数に変換)】
# - TEMPERATURE_LEVEL ∈ {0,…,10} …… 乱択度合いの調整ノブ。内部で TEMPERATURE = LEVEL / 10.0 に写像。
#     直感:0 → 決定論的(最大値同士は一様乱択のみ)/10 → 乱択強め(softmax がほぼ一様化)
#     理由:ユーザ側で意味が取りやすく、試行錯誤しやすいスケールであるため。
# - EPSILON_LEVEL ∈ {0,…,10} …… “最良近傍の相対幅(%)”。内部で EPSILON = LEVEL / 100.0 に写像。
#     直感:0 → 最良値のみ採用/10 → 最良値 +10% までを“ほぼ同等”として許容
#     理由:期待残余候補数は局面によって差が小さいことが多く、相対幅での許容が解釈しやすい。
# - RNG_SEED(整数 or None) …… 乱数の再現性制御。対人戦では None(都度変動)を推奨、再現検証では固定値が便利。
#
# 【デフォルトと調整の指針】
# - 既定:TEMPERATURE_LEVEL=2(控えめな乱択)、EPSILON_LEVEL=1(+1% 近傍)。
#   → 解探索の安定性と“読まれにくさ”のバランスが良い初期設定。
# - 対人戦で相手の読みを外したい:TEMPERATURE_LEVEL を 3〜4 に上げる(多様化)。散漫に感じたら 1 へ下げる。
# - 近傍幅の微調整:まず 0〜2 の範囲で。0 は厳格最良のみ、2 は +2% まで許容。
#
# ───────────────────────────────────────────────────────────
# ユーザ設定(0〜10 の整数スケール。内部で実数に変換される)
RNG_SEED          = None  # None: 実行毎に変動/int: 乱数を固定(例: 12345)
TEMPERATURE_LEVEL = 2     # 0–10(乱択度合い)→ TEMPERATURE = LEVEL / 10.0
EPSILON_LEVEL     = 1     # 0–10(最良相対近傍%)→ EPSILON = LEVEL / 100.0

# 内部変換(アルゴリズムが用いる実数値)
TEMPERATURE = TEMPERATURE_LEVEL / 10.0  # 例: 2→0.2
EPSILON     = EPSILON_LEVEL / 100.0     # 例: 1→0.01
# ───────────────────────────────────────────────────────────

from itertools import permutations
from collections import defaultdict
import math, random

# U:許容コード全体(10P3 = 720)。各桁は相異(重複なし)。
def all_codes():
    return [''.join(p) for p in permutations('0123456789', 3)]

UNIVERSE = all_codes()

# 応答写像 hb : (secret, guess) ↦ (H, B)
# H:座位一致数、B:値一致数から H を控除した個数(= “位置違い”の一致数)。
def hb(secret: str, guess: str):
    H = sum(1 for i in range(3) if secret[i] == guess[i])
    B = sum(1 for ch in guess if ch in secret) - H
    return (H, B)

# partition_sizes:現候補 S を hb(·, g) によって (H,B) ごとに分割し、各同値類の濃度を返す。
def partition_sizes(cands, g: str):
    buckets = defaultdict(int)
    for s in cands:
        buckets[hb(s, g)] += 1
    return buckets  # 例:{(1,0): 12, (0,2): 8, ...}

# metrics_on:指標の一括計算。
# - worst(最大残余候補数)= max_i b_i
# - exp_rem(期待残余候補数)= Σ_i b_i^2 / |S|
# - entropy(エントロピー)= -Σ_i (b_i/|S|) log2(b_i/|S|)
def metrics_on(cands, g: str):
    n = len(cands)
    if n == 0:
        return {'worst': 0, 'exp_rem': 0.0, 'entropy': 0.0, 'buckets': {}}
    buckets = partition_sizes(cands, g)
    worst = max(buckets.values()) if buckets else 0
    exp_rem = sum(b*b for b in buckets.values()) / n
    Hbits = 0.0
    for b in buckets.values():
        p = b / n
        Hbits -= p * math.log(p, 2)
    return {'worst': worst, 'exp_rem': exp_rem, 'entropy': Hbits, 'buckets': buckets}

# softmax_choice:温度 T のソフトマックスで items から 1 点を確率的に選ぶ。
#   score_fn は「大きいほど良い」スコアを返す関数。
def softmax_choice(items, score_fn, temperature):
    vals = [score_fn(x) for x in items]
    if temperature <= 0:
        # 極限:最大値同士の一様乱択(決定論的な近似)
        maxv = max(vals)
        pool = [it for it, v in zip(items, vals) if v == maxv]
        return random.choice(pool)
    mx = max(vals)
    exps = [math.exp((v - mx) / temperature) for v in vals]
    z = sum(exps)
    r = random.random() * z
    acc = 0.0
    for it, e in zip(items, exps):
        acc += e
        if acc >= r:
            return it
    return items[-1]

# hybrid_select(本版の要):期待残数→近傍→エントロピー→乱択 の順で選抜する。
# 直感:
#   ① Σ b_i^2 / |S|(“次手の期待的な取り残し”)を最小化。
#   ② “ほぼ最良”集合(相対誤差 ≤ EPSILON)に限定。
#   ③ 近傍内でエントロピー(情報量)を指標に優先順位付け。
#   ④ 最後に softmax で多様化(対称性と対人耐性を確保)。
def hybrid_select(cands, G):
    stats = {g: metrics_on(cands, g) for g in G}
    min_exp = min(stats[g]['exp_rem'] for g in G)
    # “ほぼ最良”の相対近傍:min_exp * (1 + EPSILON) 以下。
    near = [g for g in G if stats[g]['exp_rem'] <= (1.0 + EPSILON) * min_exp + 1e-12]
    # 近傍内ではエントロピーをスコアとして softmax 乱択。
    picked = softmax_choice(near, lambda g: stats[g]['entropy'], TEMPERATURE)
    s = stats[picked]
    return picked, s['worst'], s['exp_rem'], s['entropy']

# 参考:最悪分割(最大同値類濃度)を最小化するミニマックス。
def minimax_over(cands, G):
    best_g, best_worst, best_is_candidate = None, float('inf'), False
    for g in G:
        w = metrics_on(cands, g)['worst']
        is_cand = (g in cands)
        # 同点時は(i)候補内優先、(ii)語順最小で裁く。
        key = (w, 0 if is_cand else 1, g)
        if best_g is None or key < (best_worst, 0 if best_is_candidate else 1, best_g):
            best_g, best_worst, best_is_candidate = g, w, is_cand
    return best_g, best_worst, best_is_candidate

# 参考:情報量(エントロピー)最大化。
def entropy_choice(cands, G):
    best_g, best_Hbits, best_is_candidate = None, -1.0, False
    for g in G:
        Hbits = metrics_on(cands, g)['entropy']
        is_cand = (g in cands)
        key = (-Hbits, 0 if is_cand else 1, g)
        if best_g is None or key < (-best_Hbits, 0 if best_is_candidate else 1, best_g):
            best_g, best_Hbits, best_is_candidate = g, Hbits, is_cand
    return best_g, best_Hbits, best_is_candidate

# 刈り込み作用素:S ← { s ∈ S | hb(s, guess) = (H, B) }。
def filter_candidates(cands, guess: str, H: int, B: int):
    return [s for s in cands if hb(s, guess) == (H, B)]

class HBSession:
    def __init__(self):
        # 乱数シード(None なら run-to-run で変動)
        if RNG_SEED is not None:
            random.seed(RNG_SEED)
        self.candidates = UNIVERSE.copy()       # 現候補集合 S
        self.prev_candidates = UNIVERSE.copy()  # 入力適用“直前”の S(3H0B 表示用)
        self.history = []                       # 履歴列 [(guess, H, B), ...]
        self.first_move = True                  # 初回表示フラグ(「最初の推測」用)
        self.stack = []                         # undo 用:適用前状態のスタック

    # 入力検証:'xyz:mn' 形式・桁相異・値域制約(違反時は例外)
    def parse_entry(self, entry: str):
        entry = entry.strip()
        if ':' not in entry:
            raise ValueError("入力は 'xyz:mn' の形式である必要があります(コロンがありません)。")
        guess, fb = entry.split(':', 1)
        guess, fb = guess.strip(), fb.strip()
        if len(guess) != 3 or not guess.isdigit():
            raise ValueError("推測は3桁の数字でなければなりません(例 '012')。")
        if len(set(guess)) != 3:
            raise ValueError("各桁は相異でなければなりません(重複不可)。")
        if len(fb) != 2 or not fb.isdigit():
            raise ValueError("応答は2桁の数字 'mn' である必要があります(例 '10' は1H1B)。")
        H, B = int(fb[0]), int(fb[1])
        if not (0 <= H <= 3 and 0 <= B <= 3 and H + B <= 3):
            raise ValueError("無効なH/B(0 ≤ H,B ≤ 3 かつ H+B ≤ 3)。")
        return guess, H, B

    # 状態遷移:'xyz:mn' を適用。返値は (|S|, status)、status ∈ {None, 'solved_by_3h0b', 'solved_by_unique'}。
    def update(self, entry: str):
        # undo 用に適用前の状態を保存
        self.stack.append((
            list(self.candidates),
            list(self.history),
            list(self.prev_candidates),
            self.first_move
        ))
        # 3H0B 時に“直前 S”を表示するため控える
        self.prev_candidates = list(self.candidates)

        guess, H, B = self.parse_entry(entry)

        if H == 3 and B == 0:
            # 全一致。内部的には candidates を一意化するが、終了時の表示は prev_candidates を用いる。
            self.history.append((guess, H, B))
            self.candidates = [guess]
            return 1, 'solved_by_3h0b'

        # 通常の刈り込み
        self.candidates = filter_candidates(self.candidates, guess, H, B)
        self.history.append((guess, H, B))

        if len(self.candidates) == 1:
            return 1, 'solved_by_unique'
        return len(self.candidates), None

    # 1手巻き戻し(成功 True/不可 False)
    def undo(self):
        if not self.stack:
            return False
        cands, hist, prev, first = self.stack.pop()
        self.candidates, self.history = cands, hist
        self.prev_candidates, self.first_move = prev, first
        return True

    # 推奨手(従来 3 方策+ハイブリッド 2 種)を同時計算して返す。
    def recommend_all(self):
        S, U = self.candidates, UNIVERSE

        # 参考方策
        g1, w1, c1 = minimax_over(S, S)   # 候補制限ミニマックス(G=S)
        g2, w2, c2 = minimax_over(S, U)   # 全体ミニマックス(G=U、評価は S)
        g3, H3, c3 = entropy_choice(S, U) # エントロピー最大化

        # ハイブリッド(G=S と G=U)
        ghS, wS, eS, hS = hybrid_select(S, S)
        ghU, wU, eU, hU = hybrid_select(S, U)

        return {
            'candidate_minimax':    {'guess': g1,  'max_remaining': w1, 'in_candidates': c1},
            'unrestricted_minimax': {'guess': g2,  'max_remaining': w2, 'in_candidates': c2},
            'entropy':              {'guess': g3,  'entropy_bits': H3,  'in_candidates': c3},
            'hybrid_in_S':          {'guess': ghS, 'max_remaining': wS, 'exp_remaining': eS, 'entropy_bits': hS, 'in_candidates': True},
            'hybrid_in_U':          {'guess': ghU, 'max_remaining': wU, 'exp_remaining': eU, 'entropy_bits': hU, 'in_candidates': (ghU in S)},
        }

# 集合関係の表示ラベル
def in_set_label(flag: bool) -> str:
    return '解答候補に含まれる' if flag else '解答候補に含まれない'

if __name__ == '__main__':
    # RNG_SEED は HBSession.__init__ でも適用しているが、冪等のためここでも明示
    if RNG_SEED is not None:
        random.seed(RNG_SEED)

    s = HBSession()
    print('ヒット&ブロー(3桁・各桁相異)— hybrid(期待値+情報量)複数方策アシスタント/undo付き(v11c-jp)')
    print("「undo」または「取り消し」で直前の手に戻ります。「終了/quit」で終了します。\n")

    while True:
        # 矛盾的入力の累積などにより S=∅ になった場合は終了(前段の入力列に不整合がある可能性)
        if not s.candidates:
            print('候補が存在しません。以前の入力を確認してください。')
            break

        rec = s.recommend_all()
        label = '最初の推測' if s.first_move else '次の推測'
        s.first_move = False

        # 推奨手の提示(評価語は “最大残余候補数/エントロピー期待値/期待残余候補数/解答候補に含まれる …” に統一)
        print(f'{label}:')
        print(f"  候補制限ミニマックス : {rec['candidate_minimax']['guess']}  "
              f"[最大残余候補数={rec['candidate_minimax']['max_remaining']}; {in_set_label(rec['candidate_minimax']['in_candidates'])}]")
        print(f"  全体ミニマックス     : {rec['unrestricted_minimax']['guess']}  "
              f"[最大残余候補数={rec['unrestricted_minimax']['max_remaining']}; {in_set_label(rec['unrestricted_minimax']['in_candidates'])}]")
        print(f"  エントロピー最大化   : {rec['entropy']['guess']}  "
              f"[エントロピー期待値={rec['entropy']['entropy_bits']:.4f}bit; {in_set_label(rec['entropy']['in_candidates'])}]")
        print(f"  ハイブリッド(S 内) : {rec['hybrid_in_S']['guess']}  "
              f"[期待残余候補数={rec['hybrid_in_S']['exp_remaining']:.2f}; エントロピー={rec['hybrid_in_S']['entropy_bits']:.4f}bit; "
              f"最大残余候補数={rec['hybrid_in_S']['max_remaining']}; 解答候補に含まれる]")
        print(f"  ハイブリッド(U 全) : {rec['hybrid_in_U']['guess']}  "
              f"[期待残余候補数={rec['hybrid_in_U']['exp_remaining']:.2f}; エントロピー={rec['hybrid_in_U']['entropy_bits']:.4f}bit; "
              f"最大残余候補数={rec['hybrid_in_U']['max_remaining']}; {in_set_label(rec['hybrid_in_U']['in_candidates'])}]")
        print(f'  残り候補数           : {len(s.candidates)}')

        # 入力インターフェース(“XYZ:HB” 見出しは仕様)
        print('XYZ:HB')
        line = input('> ').strip()

        # 終了コマンド
        if line in ('終了', 'quit', 'exit', 'QUIT', 'EXIT', 'q', 'Q'):
            print('終了します。')
            break

        # undo(直前状態へ巻き戻し)
        if line.lower() == 'undo' or line == '取り消し':
            if s.undo():
                print(f'取り消しました。現在の候補数: {len(s.candidates)}\n')
            else:
                print('取り消せません(すでに初期状態です)。\n')
            continue

        # 通常の状態更新
        try:
            rem, status = s.update(line)

            # 3H0B による確定:直前の S を列挙
            if status == 'solved_by_3h0b':
                print('確定')
                print(' '.join(sorted(s.prev_candidates)))
                break

            # 一意化による確定:現 S(1 元集合)を列挙
            if status == 'solved_by_unique':
                print('確定')
                print(' '.join(sorted(s.candidates)))
                break

        except Exception as e:
            # 入力形式・値域違反・桁重複などはここで通知し、ループを継続
            print(f'エラー: {e}。再入力してください。\n')
            continue

追記

実はファミコン実機の場合は、乱数に制限があるようで考察サイトが存在しますので、ご紹介します。
女神転生2(FC版)コードブレイカー(ナンディさん)を攻略・考察する

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

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?