概要
「極妙幻境」の第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)
テスト仕様
以下のテストシナリオを実装し、動作を検証する:
- 攻撃フェーズ・手牌豊富:出牌枚数と宣言が期待通りか検証(乱数シード固定で統計確認)
- 攻撃フェーズ・手牌不足:手牌3枚以下時に全枚出牌を確認
- ブラフ発生率:固定シードで複数回呼び出し、ブラフ率が約18%であることを統計的に確認
- 応答・疑念提示:高疑念度状況(相手手牌少・積み札多)で
doubt返却を確認 - 応答・降参:積み札6枚以上かつBOSS手牌5枚以下で
surrender返却を確認 - 応答・追従:通常状況で
matchと妥当な枚数・宣言を確認 - 宣言妥当性の自動修正:不可能な宣言が自動的に実現可能値に補正されることを確認
- JOKER限定手牌:JOKERのみの手牌でも正常に動作することを確認