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?

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

Last updated at Posted at 2025-08-16

はじめの前のおねがい

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

はじめに

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

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

基本動作

ヒット&ブロー — 出題者版(3桁・各桁相異(重複なし))
各桁が異なる3桁を入力してください(例:012)。
コマンド:history / undo / reveal / quit

推測(3桁)> 

上記のプロンプトに

  1. 入力画面になるので3桁(4桁指定の時には4桁)のそれぞれ異なる半角数字を入力して下さい
  2. HitとBlowが0H1Bのように表示されるのでその情報を元に次の数字を入力して下さい
  3. コマンドhistoryで履歴、undoでやり直し、revealで降参、quitで終了です

と入力します。

カスタマイズ

ソースコード冒頭の

CODE_LENGTH: int = 3          # 3 または 4(既定 = 3)
ALLOW_DUPLICATES: bool = False  # False: 桁の重複 禁止(10P^L)/ True: 許容(10^L)
ALLOW_LEADING_ZERO: bool = True  # 先頭 0 を許容
SECRET_OVERRIDE: str | None = None  # 例 "780" / "0123" を指定すると出題を固定
RNG_SEED: int | None = None   # None: UTCナノ秒で初期化 / int: 再現のため固定

の部分をいじることで、数字を3桁にするか4桁にするか、重複0112など禁止するか許可するかを選択できます。また、先頭を0で始めても良いか、数字を乱数ではなく指定にするかもカスタマイズできます。

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

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)

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

謝辞

  1. コメントにて非常に有用なアドバイスをいただきました。この場にて深くお礼を申し上げます。私自身、利用歴3年なのに何やってんだ、と自分自身にツッコミを入れたいところですが、コーディングは日曜大工よりも頻度が低いために、コメントにて具体的にご教示いただけるのは非常に嬉しくもあり、ありがたくもあります。
  2. 本コードはpy 乙様のプログラミング上達講座3:メガテンのコードブレイカーの記事を大きく参考にさせていただきました。ここにお礼を申し上げます。

Pythonの必要なモジュール

ありません。

ソースコード

hit_blow_host.py
"""Hit & Blow Host:桁数(3/4)・桁重複(許容/禁止) 切替対応

   桁数(3/4)と桁の重複の可否(許容/禁止)をコード先頭の設定で切り替えられる。
   デフォルトは「重複なし・3桁」。

   乱数初期化:RNG_SEED が None の場合、UTC ナノ秒(time.time_ns)を用いる。

   操作:history / undo / reveal / quit
"""

from __future__ import annotations

import random
import time
from typing import List, Tuple

# ──────────────────────────────────────────────────────────
# 設定(必要なら変更可)
CODE_LENGTH: int = 3          # 3 または 4(既定 = 3)
ALLOW_DUPLICATES: bool = False  # False: 桁の重複 禁止(10P^L)/ True: 許容(10^L)
ALLOW_LEADING_ZERO: bool = True  # 先頭 0 を許容
SECRET_OVERRIDE: str | None = None  # 例 "780" / "0123" を指定すると出題を固定
RNG_SEED: int | None = None   # None: UTCナノ秒で初期化 / int: 再現のため固定
# ──────────────────────────────────────────────────────────


def hb(secret: str, guess: str) -> Tuple[int, int]:
    """Hit & Blow 判定。

    座位一致の個数を Hit(H)、値一致から H を控除した個数を Blow(B)とする。

    Parameters
    ----------
    secret : str
        出題(L 桁)。
    guess : str
        推測(L 桁)。

    Returns
    -------
    (int, int)
        (H, B)
    """
    length = CODE_LENGTH
    h = sum(1 for i in range(length) if secret[i] == guess[i])
    b = sum(1 for ch in guess if ch in secret) - h
    return h, b


def validate_guess(text: str) -> str:
    """推測値を検証し、正規化して返す。

    仕様:L 桁の数字。重複禁止モードでは各桁は相異。先頭 0 禁止モードでは
    先頭が '0' であってはならない。

    Parameters
    ----------
    text : str
        ユーザ入力の生文字列。

    Returns
    -------
    str
        妥当な L 桁の数値文字列。

    Raises
    ------
    ValueError
        入力が仕様を満たさない場合。
    """
    length = CODE_LENGTH
    s = text.strip()
    if len(s) != length or not s.isdigit():
        sample = "012" if length == 3 else "0123"
        raise ValueError(f"推測は {length} 桁の数字で入力してください(例:{sample})。")

    if not ALLOW_DUPLICATES and len(set(s)) != length:
        raise ValueError("各桁は相異である必要があります(重複は不可です)。")

    if not ALLOW_LEADING_ZERO and s[0] == "0":
        raise ValueError("先頭 0 は不可です。別の数を入力してください。")

    return s


def draw_secret() -> str:
    """秘密コードをモードに応じて一様分布で直接生成する。

    - 重複許容(10^L): 各桁を独立一様に選ぶ(先頭 0 禁止時は先頭のみ 1–9)。
    - 重複禁止(10P^L): random.sample を利用(先頭 0 禁止時は先頭 1–9 を選び、
      残りをそのほかから重複なしで一様抽出)。

    Returns
    -------
    str
        生成された L 桁の数値文字列。

    Raises
    ------
    ValueError
        SECRET_OVERRIDE が現行モードと整合しない場合。
    """
    if SECRET_OVERRIDE is not None:
        return validate_guess(SECRET_OVERRIDE)

    digits = "0123456789"
    length = CODE_LENGTH

    if ALLOW_DUPLICATES:
        if ALLOW_LEADING_ZERO:
            return "".join(random.choice(digits) for _ in range(length))
        first = random.choice("123456789")
        tail = "".join(random.choice(digits) for _ in range(length - 1))
        return first + tail

    # 重複禁止(10P^L)
    if ALLOW_LEADING_ZERO:
        # sample は抽出順が一様にシャッフルされるため、順列全体に対して一様。
        return "".join(random.sample(digits, length))

    # 先頭 0 禁止:先頭は 1–9、一様抽出。残りはそのほかから重複なしで一様抽出。
    first = random.choice("123456789")
    remaining_pool: List[str] = [d for d in digits if d != first]
    tail_list = random.sample(remaining_pool, length - 1)
    return first + "".join(tail_list)


def print_history(history: List[Tuple[str, int, int]]) -> None:
    """履歴を整形して標準出力へ表示する。"""
    if not history:
        print("(履歴なし)")
        return

    print("手数  推測    判定")
    for idx, (g, h, b) in enumerate(history, 1):
        print(f"{idx:>3}  {g}   {h}H{b}B")


def main() -> None:
    """メインループ。履歴、undo、降参を含む対話型の出題者版。"""
    if RNG_SEED is not None:
        random.seed(RNG_SEED)
    else:
        random.seed(time.time_ns())  # UTC ナノ秒

    secret = draw_secret()
    history: List[Tuple[str, int, int]] = []
    stack: List[List[Tuple[str, int, int]]] = []  # undo 用:履歴スナップショット
    start_ts = time.time()

    mode_len = f"{CODE_LENGTH}"
    mode_dup = "桁重複 許容" if ALLOW_DUPLICATES else "各桁相異(重複なし)"
    example = "012" if CODE_LENGTH == 3 else "0123"
    freedom = "異なる" if not ALLOW_DUPLICATES else "自由な"

    print(f"ヒット&ブロー — 出題者版({mode_len}{mode_dup}")
    print(f"各桁が{freedom}{CODE_LENGTH}桁を入力してください(例:{example})。")
    print("コマンド:history / undo / reveal / quit\n")

    while True:
        line = input(f"推測({CODE_LENGTH}桁)> ").strip()

        lower = line.lower()
        if lower in ("quit", "exit", "q"):
            print("終了します。")
            break

        if lower == "history":
            print_history(history)
            continue

        if lower == "undo":
            if not stack:
                print("取り消せません(これ以上戻れません)。")
            else:
                history = stack.pop()
                print("直前の推測を取り消しました。")
                print_history(history)
            continue

        if lower == "reveal":
            elapsed = time.time() - start_ts
            print(f"降参。解答は {secret} です。")
            print(f"試行回数:{len(history)},経過時間:{elapsed:.1f}")
            break

        try:
            guess = validate_guess(line)
        except ValueError as exc:
            print(f"エラー:{exc}")
            continue

        # 判定と記録
        stack.append(history[:])  # undo のために現履歴を保存
        h, b = hb(secret, guess)
        history.append((guess, h, b))
        print(f"判定:{h}H{b}B")

        if h == CODE_LENGTH and b == 0:
            elapsed = time.time() - start_ts
            print("\n正解")
            print(f"解答:{secret}")
            print(f"試行回数:{len(history)},経過時間:{elapsed:.1f}")
            print("\n--- 履歴 ---")
            print_history(history)
            break


if __name__ == "__main__":
    main()

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

0
0
2

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?