"""Python 引擎冒煙測試(不連網、不呼叫真實 LLM)。 以「模擬 LLM」取代 API 呼叫,跑完整場騙子酒館遊戲,驗證 game.py / player.py / game_record.py 主流程可正常完成並產生記錄。 使用方式: .venv\\Scripts\\python.exe scripts\\smoke_test_python.py (若主控台遇到簡體中文編碼錯誤,請先設定 $env:PYTHONIOENCODING="utf-8") """ import os import sys import random REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, REPO_ROOT) from game import Game class MockChat: """附著在某位玩家身上的假 LLM:依提示內容回傳格式正確的 JSON。 注意:chat() 統一回傳 (content, reasoning_content) 二元素 tuple。 """ def __init__(self, player): self.player = player self.decision_count = 0 def chat(self, messages, model=None): prompt = messages[0]["content"] if messages else "" if "played_cards" in prompt: return self._play_reply(), "" if "was_challenged" in prompt: return self._challenge_reply(), "" return "mock opinion: \u8a72\u73a9\u5bb6\u76ee\u524d\u7121\u660e\u986f\u50be\u5411\u3002", "" def _play_reply(self): """\u7d66\u4e86\u81ea\u5df1\u7684\u624b\u724c\uff1a\u540c\u9ede\u6578 >=2 \u51fa\u6700\u591a 3 \u5f35\uff0c\u5426\u5247\u51fa 1 \u5f35\u3002""" hand = sorted(self.player.hand) cards = [hand[0]] for c in hand[1:3]: if c == cards[0]: cards.append(c) cards_json = ",".join('"%s"' % c for c in cards) return ('{"played_cards": [%s], "behavior": "\u7a69\u5065", ' '"play_reason": "mock heuristic"}' % cards_json) def _challenge_reply(self): """\u6bcf 4 \u6b21\u8cea\u7591\u6c7a\u7b56\u5c31\u8cea\u7591\u4e00\u6b21\uff0c\u8986\u84cb\u6210\u529f/\u5931\u6557\u5169\u689d\u8def\u5f91\u3002""" self.decision_count += 1 do_challenge = (self.decision_count % 4 == 0) return ('{"was_challenged": %s, "challenge_reason": "mock trigger"}' % ("true" if do_challenge else "false")) def main(): random.seed(20260803) player_configs = [ {"name": "DeepSeek", "model": "mock"}, {"name": "GPT", "model": "mock"}, {"name": "Claude", "model": "mock"}, {"name": "Gemini", "model": "mock"}, ] game = Game(player_configs) for p in game.players: p.llm_client = MockChat(p) max_plays = 400 for step in range(max_plays): if game.game_over: break game.play_round() if game.game_over: print("[OK] \u904a\u6232\u7d50\u675f\uff0c\u4e3b\u6d41\u7a0b\u53ef\u6b63\u5e38\u5b8c\u6210\u3002") else: print(f"[FAIL] \u8dd1\u4e86 {max_plays} \u6b65\u4ecd\u672a\u7d50\u675f\uff08\u53ef\u80fd\u89c4\u5247\u6709\u6b7b\u7d50\uff09\u3002 rounds={game.round_count}") return 1 rec = game.game_record rounds = len(rec.rounds) alive = [p.name for p in game.players if p.alive] print(f"[OK] \u7372\u52dd\u8005={rec.winner} | \u5b58\u6d3b\u73a9\u5bb6={alive}") print(f"[OK] \u8a18\u9304\u56de\u5408\u6578={rounds}") import glob files = glob.glob(os.path.join(REPO_ROOT, "game_records", "*.json")) print(f"[OK] game_records \u7522\u51fa {len(files)} \u500b JSON \u8a18\u9304\u6a94") if files: newest = max(files, key=os.path.getmtime) print(f" \u6700\u65b0\u8a18\u9304: {os.path.basename(newest)}") return 0 if __name__ == "__main__": sys.exit(main())