0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ヒット&ブロー(3桁または4桁の数字を当てるやつ)を頑張って当てるためのコード

Last updated at Posted at 2025-08-17

はじめの前のおねがい

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

はじめに

  1. このコードはヒット&ブロー(3桁または4桁の数字を当てるやつ)を頑張って当てるためのコードを改良したコードです。
  2. このコードはPython 3Pythonista 3に対応しています。

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

基本動作

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

ヒット&ブロー(3桁・各桁が相異)
入力例:3桁 → 012:10/4桁 → 0123:10('10' は 1Hit 1Blow を意味)
「undo」または「取り消し」で直前に戻ります。「終了/quit」で終了します。

最初の推測: 896 (当たる確率 約0%)
残り候補数: 720
XYZ:HB
1> 

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

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

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

のルールで入力します。

カスタマイズ

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

CODE_LENGTH: int = 3            # 3 または 4(既定 = 3)
ALLOW_DUPLICATES: bool = False  # False: 重複なし(10P^L)/ True: 重複を許容(10^L)

の部分をいじります。

ヒットアンドブローて何?

3桁版ヒット&ブロー

ルール
プレイヤーが0〜9の数字からなる3桁の秘密の数字列(例:527)を推測する。
ヒット(Hit):数字とその位置が完全に一致する箇所の数。
ブロー(Blow):数字は正しいが位置が違う箇所の数。


答え:527
推測:572
結果:1 Hit(5)、2 Blow(2と7)

特徴
比較的短いため、ゲーム時間が短くテンポが良い。
初心者向けに適している。

4桁版ヒット&ブロー

ルール
推測すべきは4桁の秘密の数字列(例:6049)。その他のルールは3桁版と同様。


答え:6049
推測:6409
結果:2 Hit(0と9)、2 Blow(6と4)

特徴
組み合わせ数が増えるため、難易度が高い。
より論理的・戦略的な推理が求められる。
プレイ時間も長くなる傾向。

Pythonの必要なモジュール

ありません。

ソースコード

hit_blow_solver.py
"""Hit & Blow Solver:3/4 桁・重複許容切替対応

機能
  桁数切替(3/4): CODE_LENGTH
  重複許容切替(許容/禁止): ALLOW_DUPLICATES
  推奨手: ハイブリッド(S 内)— exp_rem 最小の ε近傍から entropy 指向 softmax
  当たり確率表示(100/|S| を真正の四捨五入。小数が出る場合は「約」を付す)
  1> 始まりの手数プロンプト
  S=∅(矛盾)時の自動 1 回 undo
  確定後の再開確認(1/y 再開 / 0/n/終了)

"""

from __future__ import annotations

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

# ─────────────────────────────────────────────────────
# モード設定(必要に応じて変更)
CODE_LENGTH: int = 3            # 3 または 4(既定 = 3)
ALLOW_DUPLICATES: bool = False  # False: 重複なし(10P^L)/ True: 重複を許容(10^L)
RNG_SEED: int | None = None     # 乱数シード(None なら実行時状態に依存)

# 乱択・近傍パラメータ(0〜10 の整数→内部で実数化)
TEMPERATURE_LEVEL: int = 2      # 乱択度(/10.0)
EPSILON_LEVEL: int = 1          # 最良近傍の相対幅[%](/100.0)

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


# ========== ユーティリティ ==========
def all_codes() -> List[str]:
    """全候補を設定に基づき生成する。

    - 重複なし: permutations(10P^L)
    - 重複あり: product     (10^L)
    先頭 0 は許容。
    """
    digits = "0123456789"
    lgt = CODE_LENGTH
    if ALLOW_DUPLICATES:
        return ["".join(t) for t in product(digits, repeat=lgt)]   # 10^L
    return ["".join(p) for p in permutations(digits, lgt)]         # 10P^L


def hb(secret: str, guess: str) -> Tuple[int, int]:
    """Hit & Blow 判定(H: 座位一致、B: 値一致から H を控除)。"""
    lgt = CODE_LENGTH
    h = sum(1 for i in range(lgt) if secret[i] == guess[i])
    b = sum(1 for ch in guess if ch in secret) - h
    return h, b


def partition_sizes(cands: Sequence[str], guess: str) -> Dict[Tuple[int, int], int]:
    """全候補 S を hb(·, guess) により (H,B) ごとに分割し、濃度 b_i を返す。"""
    buckets: Dict[Tuple[int, int], int] = defaultdict(int)
    for s in cands:
        buckets[hb(s, guess)] += 1
    return buckets


def metrics_on(cands: Sequence[str], guess: str) -> Dict[str, float]:
    """指標を計算する。
       - worst   = max_i b_i
       - exp_rem = Σ b_i^2 / |S|
       - entropy = - Σ (b_i/|S|) log2(b_i/|S|)"""
    n = len(cands)
    if n == 0:
        return {"worst": 0.0, "exp_rem": 0.0, "entropy": 0.0}

    buckets = partition_sizes(cands, guess)
    worst = float(max(buckets.values())) if buckets else 0.0
    exp_rem = sum(b * b for b in buckets.values()) / n

    entropy = 0.0
    for b in buckets.values():
        p = b / n
        entropy -= p * math.log(p, 2)

    return {"worst": worst, "exp_rem": exp_rem, "entropy": entropy}


def softmax_choice(items: Sequence[str], score_fn, temperature: float) -> str:
    """温度 T のソフトマックスで 1 要素を選ぶ(score_fn は“高いほど良い”)。"""
    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: Sequence[str]) -> str:
    """ハイブリッド(S 内): exp_rem 最小 → ε近傍 → entropy 指向 softmax."""
    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
    ]
    return softmax_choice(near, lambda g: stats[g]["entropy"], TEMPERATURE)


def filter_candidates(
    cands: Sequence[str], guess: str, h: int, b: int
) -> List[str]:
    """刈り込み:S ← { s ∈ S | hb(s, guess) = (h, b) }"""
    target = (h, b)
    return [s for s in cands if hb(s, guess) == target]


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


# ========== セッション ==========
class HitBlowSession:
    """Hit & Blow の 1 セッションを保持し、更新、推奨、undo を提供する。"""

    def __init__(self) -> None:
        if RNG_SEED is not None:
            random.seed(RNG_SEED)

        # 設定に基づき、開始時に全候補を再構築(設定変更を確実に反映)
        self.universe: List[str] = all_codes()
        self.candidates: List[str] = self.universe.copy()
        self.prev_candidates: List[str] = self.universe.copy()  # 確定直前の S 表示用
        self.history: List[Tuple[str, int, int]] = []           # [(guess, H, B), ...]
        self.first_move: bool = True
        self.stack: List[
            Tuple[List[str], List[Tuple[str, int, int]], List[str], bool]
        ] = []  # undo 用:適用前状態のスタック

    def parse_entry(self, entry: str) -> Tuple[str, int, int]:
        """入力 'code:mn' を検証して (guess, H, B) を返す。"""
        lgt = CODE_LENGTH
        if ":" not in entry:
            raise ValueError("入力は 'xyz:mn' 形式(コロン必須)。")

        guess_raw, fb_raw = entry.split(":", 1)
        guess, fb = guess_raw.strip(), fb_raw.strip()

        if len(guess) != lgt or not guess.isdigit():
            raise ValueError(f"推測は {lgt} 桁の数字。")

        if (not ALLOW_DUPLICATES) and len(set(guess)) != lgt:
            raise ValueError("各桁は相異でなければならない(重複不可)。")

        if len(fb) != 2 or not fb.isdigit():
            raise ValueError("応答は 2 桁 'mn'。例 '10' は 1H1B。")

        h_val, b_val = int(fb[0]), int(fb[1])
        if not (0 <= h_val <= lgt and 0 <= b_val <= lgt and h_val + b_val <= lgt):
            raise ValueError(f"無効な H/B(0 ≤ H,B ≤ {lgt} かつ H+B ≤ {lgt})。")

        return guess, h_val, b_val

    def update(self, entry: str) -> Tuple[int, str | None]:
        """1手を適用し,(候補数, status) を返す。
        status ∈ {None, 'solved_by_exact', 'solved_by_unique'}"""
        # undo のため適用前状態を保存
        self.stack.append((
            list(self.candidates),
            list(self.history),
            list(self.prev_candidates),
            self.first_move,
        ))
        self.prev_candidates = list(self.candidates)

        guess, h_val, b_val = self.parse_entry(entry)

        # 完全一致(H = L, B = 0)
        if h_val == CODE_LENGTH and b_val == 0:
            self.history.append((guess, h_val, b_val))
            self.candidates = [guess]  # 内部整合
            return 1, "solved_by_exact"

        # 刈り込み
        self.candidates = filter_candidates(self.candidates, guess, h_val, b_val)
        self.history.append((guess, h_val, b_val))

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

    def undo(self) -> bool:
        """直前状態に 1 手巻き戻す(成功: True / 不可: False)。"""
        if not self.stack:
            return False
        c, hst, prev, flag = self.stack.pop()
        self.candidates, self.history = c, hst
        self.prev_candidates, self.first_move = prev, flag
        return True

    def recommend(self) -> str:
        """推奨手(S 内ハイブリッド)を 1 つ返す。"""
        return hybrid_select_in_s(self.candidates)


# ========== 対話ループ ==========
def restart_prompt() -> bool:
    """確定後の再開可否を問い合わせる(True → 再開 / False → 終了)。"""
    while True:
        ans = input("最初からやり直しますか? [1/y=再開, 0/n/終了=終了] > ").strip()
        if ans in ("1", "y", "Y"):
            return True
        if ans in ("0", "n", "N", "終了", "quit", "exit", "q", "Q"):
            return False
        print(
            "入力を理解できませんでした。'1' または 'y' で再開、"
            "'0' または 'n' または '終了' で終了してください。"
        )


def main() -> None:
    """メイン:対話的ソルバを実行する。"""
    if RNG_SEED is not None:
        random.seed(RNG_SEED)

    mode_dup = "重複を許容" if ALLOW_DUPLICATES else "各桁が相異"
    title = (
        f"ヒット&ブロー({CODE_LENGTH}桁・{mode_dup}"
    )

    while True:  # 確定後の再開に対応する外側ループ
        sess = HitBlowSession()
        print(title)
        print("入力例:3桁 → 012:10/4桁 → 0123:10('10' は 1Hit 1Blow を意味)")
        print("「undo」または「取り消し」で直前に戻ります。「終了/quit」で終了します。\n")

        game_over = False
        while True:
            # S=∅(矛盾)なら自動で 1 回 undo して継続
            if not sess.candidates:
                if sess.undo():
                    print(
                        "候補が存在しません。直前の入力を取り消して続行します。"
                        "もう一度入力してください。\n"
                    )
                else:
                    print("候補が存在しません。初期状態にリセットします。")
                    sess = HitBlowSession()

            # 推奨手と当たり確率
            rec = sess.recommend()
            n = len(sess.candidates)
            p_exact = 100.0 / n
            p_pct = round_half_up(p_exact)
            approx = abs(p_exact - p_pct) > 1e-12

            label = "最初の推測" if sess.first_move else "次の推測"
            sess.first_move = False
            if approx:
                print(f"{label}: {rec} (当たる確率 約{p_pct}%)")
            else:
                print(f"{label}: {rec} (当たる確率 {p_pct}%)")
            print(f"残り候補数: {n}")
            print("XYZ:HB")

            # 手数つきプロンプト(1> 始まり)
            line = input(f"{len(sess.history) + 1}> ").strip()

            # 即時終了
            if line in ("終了", "quit", "exit", "q", "Q"):
                print("終了します。")
                game_over = True
                break

            # undo / 取り消し
            if line.lower() == "undo" or line == "取り消し":
                if sess.undo():
                    print(f"取り消しました。候補数: {len(sess.candidates)}\n")
                else:
                    print("取り消せません。\n")
                continue

            # 通常更新
            try:
                _, status = sess.update(line)

                if status == "solved_by_exact":
                    print("確定")
                    print(" ".join(sorted(sess.prev_candidates)))
                    if restart_prompt():
                        break
                    game_over = True
                    break

                if status == "solved_by_unique":
                    print("下記の数字で確定")
                    print(" ".join(sorted(sess.candidates)))
                    if restart_prompt():
                        break
                    game_over = True
                    break

            except Exception as exc:  # 入力検証エラー等
                print(f"エラー: {exc}。再入力してください。\n")
                continue

        if game_over:
            break


if __name__ == "__main__":
    main()

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

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?