カードゲームにおけるBOSS AI意思決定関数の実装

概要

「極妙幻境」の第4・7・10層クリア後に発生するBOSS戦において、BOSS(心魔)の出牌戦略を制御するAIロジックを実装する。この関数は現在のゲーム状態を入力とし、BOSSが実行すべきアクション(出牌、疑念提示、追従、降参)を決定する純粋関数として設計する。

データ構造定義

from enum import Enum
from typing import Dict, Optional, List
import random

class Suit(Enum):
    VIRTUE = "virtue"
    VICE = "vice"
    NULL = "null"
    JOKER = "joker"

# 手牌は各スートの枚数を保持
CardHand = Dict[Suit, int]

class BoardState:
    """盤面状況"""
    boss_cards: CardHand           # BOSSの手牌
    opponent_card_count: int       # 対戦相手の手牌総数
    pile_size: int                 # 場に積まれたカード枚数
    previous_declare: Optional[Suit]  # 直前の宣言スート
    is_active_phase: bool          # True: BOSSの攻撃ターン、False: 応答ターン
    elapsed_rounds: int = 0        # 経過ラウンド数(戦略調整用)

アクション戻り値仕様

BOSSの攻撃ターン時:

{
    "move": "attack",
    "quantity": int,        # 1〜3枚、手牌上限遵守
    "declare": Suit         # 宣言するスート
}

BOSSの応答ターン時:

{
    "move": "doubt" | "match" | "surrender",
    # matchの場合は以下も必要
    "quantity": int,
    "declare": Suit
}

ユーティリティ関数

def calculate_hand_size(cards: CardHand) -> int:
    """手牌総枚数を算出"""
    return sum(cards.values())

def find_dominant_suit(cards: CardHand) -> Optional[Suit]:
    """最も所持数の多い通常スートを特定(JOKER除外)"""
    regular_suits = [Suit.VIRTUE, Suit.VICE, Suit.NULL]
    counts = {s: cards.get(s, 0) for s in regular_suits}
    max_val = max(counts.values())
    if max_val == 0:
        return None
    for s, c in counts.items():
        if c == max_val:
            return s
    return None

def validate_declaration(cards: CardHand, declared: Suit, qty: int) -> bool:
    """宣言の妥当性検証(JOKERで補填可能か)"""
    base = cards.get(declared, 0)
    jokers = cards.get(Suit.JOKER, 0)
    return base + jokers >= qty

def weighted_random_select(items: List, weights: List[float]):
    """重み付きランダム選択"""
    return random.choices(items, weights=weights)[0]

BOSS攻撃フェーズのロジック

def execute_attack_phase(state: BoardState) -> dict:
    hand_total = calculate_hand_size(state.boss_cards)
    
    # 枚数決定
    if hand_total <= 3:
        qty = hand_total
    else:
        qty = weighted_random_select([1, 2, 3], [0.25, 0.55, 0.20])
    
    # 宣言スート決定
    BLUFF_RATE = 0.18
    
    if random.random() < BLUFF_RATE:
        # ブラフ:手持ち最多スートと異なるものを宣言
        dominant = find_dominant_suit(state.boss_cards)
        alternatives = [s for s in Suit if s != dominant and s != Suit.JOKER]
        if alternatives:
            declare = random.choice(alternatives)
        else:
            declare = dominant if dominant else random.choice([Suit.VIRTUE, Suit.VICE, Suit.NULL])
    else:
        # 正直宣言
        dominant = find_dominant_suit(state.boss_cards)
        declare = dominant if dominant else random.choice([Suit.VIRTUE, Suit.VICE, Suit.NULL])
    
    # 宣言可能性の確認と修正
    if not validate_declaration(state.boss_cards, declare, qty):
        dominant = find_dominant_suit(state.boss_cards)
        if dominant is None:
            declare = Suit.VIRTUE
            qty = min(qty, state.boss_cards.get(Suit.JOKER, 0))
        else:
            available = state.boss_cards.get(dominant, 0) + state.boss_cards.get(Suit.JOKER, 0)
            qty = min(qty, available)
            declare = dominant
    
    return {"move": "attack", "quantity": qty, "declare": declare}

BOSS応答フェーズのロジック

def execute_response_phase(state: BoardState) -> dict:
    # 疑念度の算出
    doubt_factor = 0.28
    
    if state.opponent_card_count <= 3:
        doubt_factor += 0.22
    
    if state.pile_size >= 5:
        doubt_factor += 0.18
    
    doubt_factor = min(1.0, doubt_factor)
    
    # 疑念提示判定(閾値0.58)
    if doubt_factor > 0.58:
        return {"move": "doubt"}
    
    # 降参判定
    boss_total = calculate_hand_size(state.boss_cards)
    if state.pile_size >= 6 and boss_total <= 5:
        return {"move": "surrender"}
    
    # 追従
    if boss_total <= 3:
        qty = boss_total
    else:
        qty = weighted_random_select([1, 2, 3], [0.28, 0.52, 0.20])
    
    # 宣言決定(ブラフ確率18%)
    BLUFF_RATE = 0.18
    if random.random() < BLUFF_RATE:
        dominant = find_dominant_suit(state.boss_cards)
        candidates = [s for s in Suit if s != dominant and s != Suit.JOKER]
        declare = random.choice(candidates) if candidates else (dominant or Suit.VIRTUE)
    else:
        dominant = find_dominant_suit(state.boss_cards)
        declare = dominant if dominant else random.choice([Suit.VIRTUE, Suit.VICE, Suit.NULL])
    
    # 妥当性確認
    if not validate_declaration(state.boss_cards, declare, qty):
        dominant = find_dominant_suit(state.boss_cards)
        if dominant is None:
            declare = Suit.VIRTUE
            qty = min(qty, state.boss_cards.get(Suit.JOKER, 0))
        else:
            usable = state.boss_cards.get(dominant, 0) + state.boss_cards.get(Suit.JOKER, 0)
            qty = min(qty, usable)
            declare = dominant
    
    return {"move": "match", "quantity": qty, "declare": declare}

メイン決定関数

def decide_boss_action(state: BoardState) -> dict:
    """BOSSの行動を決定するエントリポイント"""
    if state.is_active_phase:
        return execute_attack_phase(state)
    else:
        return execute_response_phase(state)

テスト仕様

以下のテストシナリオを実装し、動作を検証する:

  1. 攻撃フェーズ・手牌豊富:出牌枚数と宣言が期待通りか検証(乱数シード固定で統計確認)
  2. 攻撃フェーズ・手牌不足:手牌3枚以下時に全枚出牌を確認
  3. ブラフ発生率:固定シードで複数回呼び出し、ブラフ率が約18%であることを統計的に確認
  4. 応答・疑念提示:高疑念度状況(相手手牌少・積み札多)でdoubt返却を確認
  5. 応答・降参:積み札6枚以上かつBOSS手牌5枚以下でsurrender返却を確認
  6. 応答・追従:通常状況でmatchと妥当な枚数・宣言を確認
  7. 宣言妥当性の自動修正:不可能な宣言が自動的に実現可能値に補正されることを確認
  8. JOKER限定手牌:JOKERのみの手牌でも正常に動作することを確認

タグ: Python AI ゲーム開発 カードゲーム アルゴリズム

7月5日 22:04 投稿