hanabiapp.game.game_manager
ゲームロジックを記述するモジュール
以下のコードを参考にしています: GitHub - yawgmoth/pyhanabi: A research-focused, Python implementation of Hanabi, including a browser-based user interface https://github.com/yawgmoth/pyhanabi
また,随所Hanabi-Learning-Environmentの仕様に準拠するため,以下のリポジトリを参考にしています: GitHub - google-deepmind/hanabi-learning-environment: hanabi_learning_environment is a research platform for Hanabi experiments. https://github.com/google-deepmind/hanabi-learning-environment
Hanabi-Learning-EnvironmentのことをHLEと略して表記しています.
1"""ゲームロジックを記述するモジュール 2 3* 以下のコードを参考にしています: 4GitHub - yawgmoth/pyhanabi: A research-focused, Python implementation of Hanabi, including a browser-based user interface 5https://github.com/yawgmoth/pyhanabi 6 7* また,随所Hanabi-Learning-Environmentの仕様に準拠するため,以下のリポジトリを参考にしています: 8GitHub - google-deepmind/hanabi-learning-environment: hanabi_learning_environment is a research platform for Hanabi experiments. 9https://github.com/google-deepmind/hanabi-learning-environment 10 11* Hanabi-Learning-EnvironmentのことをHLEと略して表記しています. 12""" 13 14import random 15import datetime 16from .hint_knowledge_manager import KnowledgeManager 17from .game_data_manager import GameDataManager 18from .agent.websocket_human_agent import WebsocketHumanAgent 19from .game_const import GameConst 20from .action import Action 21 22class Game: 23 """ゲーム全体の状態を管理するクラス 24 25 Attributes: 26 game_const(GameConst): ゲームの定数 27 players(list): 参加するプレイヤーのリスト 28 max_hints(int): ヒントトークンの最大数.(デフォルトは8個) 29 hints(int): 青トークンの数.hintを出せる回数としてhintsと記載 30 max_miss(int): 赤トークンの最大数.(デフォルトは3個) 31 miss(int): 赤トークンの数.プレイヤーがミスできる回数としてmissと記載. 32 board(dict): ボード上の各色のカードの最高ランク.初期状態として0を設定.最大5 33 trash(list): 捨てられたカードのリスト 34 deck(list): 山札 35 hands(dict): プレイヤーごとの手札.プレイヤー番号をキーとする辞書. 36 hand_size(int): プレイヤーの手札の枚数.2,3人プレイなら5枚,4,5人プレイなら4枚 37 current_player(int): 現在のプレイヤーを示す数字.プレイヤーのインデックスに対応. 38 ファーストプレイヤーを入れ替える場合,current_playerの初期値を変更するのではなく, 39 plauersリスト内で各プレイヤーに割り当てるindexを変更する形で対応すること 40 extra_turns(int): 残りエクストラターンの数. 41 山札切れの場合,エクストラターンに突入.extra_turnsがルール規定数分増え,0になるまで減りながらゲームが進行. 42 knowledge_manager(KnowledgeManager): カードの知識を管理するクラスのインスタンス 43 knowledge(list): カードの知識を格納した配列 44 """ 45 46 def __init__(self, players, turn_time_limit, seed=None, is_cui=True): 47 """ 48 ゲームの初期化 49 50 Args: 51 players (list): 参加するプレイヤーのリスト 52 turn_time_limit (int): 1ターンの制限時間(秒) 53 seed (int, optional): 乱数シード(デフォルトはNone) 54 is_cui (bool, optional): CUIモードで実行するか(デフォルトはTrue) 55 """ 56 self.seed = seed 57 self.random = random.Random(self.seed) 58 59 self.is_cui = is_cui # CUI表示を行うかどうかのフラグ 60 61 self.game_const = GameConst() 62 self.max_hints = 8 63 self.hints = self.max_hints 64 self.max_miss = 3 65 self.miss = self.max_miss 66 self.board = {color: 0 for color in self.game_const.ALL_COLORS} 67 self.trash = [] 68 self.deck = self.make_deck() 69 self.extra_turns = 0 70 self.turn = 0 71 self.current_player = 0 72 self.gui_log_history = [] # GUI表示用のアクション履歴 73 self.game_start_time = self.get_now_unix() # ゲーム開始時間 74 self.game_end_time = 0 # ゲーム終了時間 75 self.turn_time_limit = turn_time_limit # ターン制限時間(秒) 76 self.game_end_reason = "NOT_END" # ゲーム終了理由 77 self.num_of_valid_actions = 0 # 有効なアクション数 78 79 # 1ターンの時間計測用 80 self.turn_start_time = self.game_start_time # ターン開始時間(1ターン目はゲーム開始時間) 81 self.turn_perform_time = 0 # アクション実行後の時間 82 83 84 """プレイ人数に応じた初期化""" 85 self.players = players 86 for player in self.players: 87 player.set_game(self) 88 self.hands = {player.player_number: [] for player in players} 89 self.hand_size = 5 if len(players) in [2, 3] else 4 # 2,3人プレイなら5枚,4,5人プレイなら4枚 90 self.knowledge_manager = KnowledgeManager(self) 91 self.knowledge = self.knowledge_manager.initialize_all_knowledge() 92 self.make_hands() # 各プレイヤに初期手札を配る 93 94 # DataHandlerにゲーム開始時のデータをセット 95 self.data_manager = GameDataManager() 96 deck_str = "-".join([self.format_card(card) for card in self.deck]) 97 self.data_manager.set_data_game_start(players = self.players, turn_time_limit = self.turn_time_limit, deck = deck_str) 98 99 def print_log(self, log, arg=None): 100 """ 101 ログを出力する関数.CUIモードの場合にのみ表示する. 102 103 Args: 104 log (str): 出力するログメッセージ 105 arg (optional): 追加の引数(デフォルトはNone) 106 """ 107 if self.is_cui: 108 if arg is None: 109 print(log) 110 else: 111 log = log + ":" + str(arg) 112 print(log) 113 114 def make_deck(self): 115 """デッキを作成し、シャッフル 116 Returns: 117 deck(list): 山札 118 """ 119 deck = [] 120 for color in self.game_const.ALL_COLORS: # 全色に対して繰り返す 121 # 各色の規定カード枚数分繰り返す(デフォルト:1が3枚,2,3,4が2枚,5が1枚.) 122 for number, count in enumerate(self.game_const.COUNTS): 123 for _ in range(count): 124 deck.append((color, number + 1)) # 山札にカードを追加 125 # self.print_log("bf_deck",deck) 126 # self.print_log(self.random.random()) 127 self.random.shuffle(deck) # デッキをシャッフル 128 # self.print_log("af_deck",deck) 129 return deck 130 131 def make_hands(self): 132 """各プレイヤーに初期手札を配る""" 133 # for player in self.players: 134 # for _ in range(self.hand_size): # プレイヤー数に応じた手札枚数を配る 135 # card = self.deck.pop() # デッキからカードを引く 136 # self.hands[player.player_number].append(card) # 手札に追加 137 138 for player in self.players: 139 for _ in range(self.hand_size): # プレイヤー数に応じた手札枚数を配る 140 self.hands[player.player_number].append(self.deck[0]) # 手札に追加 141 del self.deck[0] 142 143 def draw_card(self, player_number): 144 """カードを引く 145 Args: 146 player_number(int): カードを引くプレイヤーの番号 147 """ 148 if len(self.deck) > 1: # デッキに2枚以上カードが残っている場合 149 self.hands[player_number].append(self.deck[0]) # 手札に追加 150 self.knowledge_manager.update_knowledge_after_draw(player_number) # カードを引いた後の知識更新 151 del self.deck[0] # デッキからカードを削除 152 elif len(self.deck) == 1: # デッキに残り1枚の場合 153 self.hands[player_number].append(self.deck[0]) # 手札に追加 154 self.knowledge_manager.update_knowledge_after_draw(player_number) # カードを引いた後の知識更新 155 del self.deck[0] # デッキからカードを削除 156 # 山札が0枚になったタイミングでエクストラターンを開始 157 self.print_and_append_gui_log("山札が切れました.") 158 self.print_and_append_gui_log("各プレイヤーに1ターンずつExtraターンを与えます.") 159 self.extra_turns = len(self.players)+1 # ゲーム進行処理上,エクストラターンはプレイヤー数+1回(next_turnで減算されるため) 160 else: 161 # デッキが既に空の場合は何もしない(エクストラターン中の可能性があるため,ここで終了処理しないこと) 162 pass 163 164################################################################################ 165# ここからhle_converter移植 166 def get_current_player_offset(self, observer_player): 167 """ 168 観察者から見た現在のプレイヤーまでのオフセットを計算する関数 169 170 Args: 171 observer_player (int): 観察者プレイヤーのインデックス 172 173 Returns: 174 int: 現在のプレイヤーまでのオフセット 175 """ 176 # 観察者から見た現在のプレイヤーまでのオフセットを計算 177 current_player_offset = (self.current_player - observer_player) % len(self.players) 178 return current_player_offset 179 180 def get_observed_hands(self, observer_player): 181 """ 182 指定したプレイヤーが観察できる手札を取得する関数 183 184 Args: 185 observer_player (int): 観察者プレイヤーのインデックス 186 187 Returns: 188 list: 観察者が見た手札のリスト(自身の手札の情報は非表示) 189 """ 190 observed_hands = [] 191 num_players = len(self.players) 192 193 for i in range(num_players): 194 player_index = (observer_player + i) % num_players # オフセットに基づき、順番を正しく取得 195 if player_index == observer_player: 196 # 観察者自身の手札は色とランクがわからないため、Noneと-1で表示 197 observed_hands.append([{'color': None, 'rank': -1} for _ in self.hands[player_index]]) 198 else: 199 # 他のプレイヤーの手札は実際のカード情報を返す 200 observed_hands.append([ 201 {'color': self.game_const.COLOR_NAMES[card[0]], 'rank': card[1]-1} # HLEはランクが0から始まるため-1 202 for card in self.hands[player_index] 203 ]) 204 return observed_hands 205 206 # 色または数字が確定しているかどうかを確認するヘルパーメソッド 207 def is_color_determined(self, card_knowledge): 208 """ 209 カードの色が確定しているかを判定する関数 210 211 Args: 212 card_knowledge (list): カードの知識情報 213 214 Returns: 215 str or None: 確定している場合は色名('B', 'G', 'R', 'W', 'Y'),未確定ならNone 216 """ 217 #self.print_log(f"card_knowledge:{card_knowledge}") 218 color_possibility = [] # BGRWY 219 220 for each_card_knowledge in card_knowledge: 221 #self.print_log(f"each_card_knowledge:{each_card_knowledge}") 222 #self.print_log(f"sum:{sum(each_card_knowledge)}") 223 # その色の知識が存在するならcolor_possibilityに1を追加,そうでないなら0を追加 224 if sum(each_card_knowledge) > 0: 225 color_possibility.append(1) 226 else: 227 color_possibility.append(0) 228 229 #self.print_log(color_possibility) 230 # color_possibilityの合計が1ならば、1色に確定しているので,その色を返す 231 if sum(color_possibility) == 1: 232 for j, item in enumerate(color_possibility): 233 if item == 1: 234 return self.game_const.COLOR_NAMES[j] 235 236 return None 237 238 def is_rank_determined(self, card_knowledge): 239 """ 240 カードのランクが確定しているかを判定する関数 241 242 Args: 243 card_knowledge (list): カードの知識情報 244 245 Returns: 246 int or None: 確定している場合はランク(0〜4),未確定ならNone 247 """ 248 #self.print_log(f"card_knowledge:{card_knowledge}") 249 number_possibility = [] # 01234 250 251 for num in range(5): 252 is_exist_num_knowledge = False 253 for each_card_knowledge in card_knowledge: 254 255 # numの知識が存在するなら1を追加 256 if each_card_knowledge[num] > 0: 257 is_exist_num_knowledge = True 258 259 if is_exist_num_knowledge: 260 number_possibility.append(1) 261 else: 262 number_possibility.append(0) 263 264 #self.print_log(number_possibility) 265 266 # number_possibilityの合計が1ならば、1つの数字に確定しているので,その数字を返す 267 if sum(number_possibility) == 1: 268 for j, item in enumerate(number_possibility): 269 if item == 1: 270 return j 271 272 273 def get_card_knowledge(self): 274 """ 275 各プレイヤーの手札に関する知識を取得する関数 276 277 Returns: 278 list: 各プレイヤーのカード知識を表すリスト 279 """ 280 card_knowledge = [] 281 for player_number in range(len(self.players)): 282 player_knowledge = [] 283 for each_card_knowledge in self.knowledge[player_number]: 284 #self.print_log(f"card:{each_card_knowledge}") 285 card_info = {'color': None, 'rank': None} 286 287 # 色の確定状況をチェック 288 card_info['color'] = self.is_color_determined(each_card_knowledge) 289 290 # ランクの確定状況をチェック 291 card_info['rank'] = self.is_rank_determined(each_card_knowledge) 292 293 player_knowledge.append(card_info) 294 card_knowledge.append(player_knowledge) 295 return card_knowledge 296 297 def action_to_hle_move(self, action): 298 """ 299 Hanabi Learning Environment(HLE)のアクション形式に変換する関数 300 301 Args: 302 action (Action): 変換するアクション 303 304 Returns: 305 dict: HLE形式のアクション辞書 306 """ 307 move = {} 308 if action.type == self.game_const.PLAY: 309 move['action_type'] = 'PLAY' 310 move['card_index'] = action.card_position 311 elif action.type == self.game_const.DISCARD: 312 move['action_type'] = 'DISCARD' 313 move['card_index'] = action.card_position 314 elif action.type == self.game_const.HINT_COLOR: 315 move['action_type'] = 'REVEAL_COLOR' 316 move['target_offset'] = self.get_current_player_offset(action.pnr) # プレイヤーのオフセットを計算 317 move['color'] = self.game_const.COLOR_NAMES[action.color] # ヒントの色 318 elif action.type == self.game_const.HINT_NUMBER: 319 move['action_type'] = 'REVEAL_RANK' 320 move['target_offset'] = self.get_current_player_offset(action.pnr) # プレイヤーのオフセットを計算 321 move['rank'] = action.number # ヒントの数字 322 return move 323 324 def create_observation(self, observer_player): 325 """ 326 指定したプレイヤーの観察データを作成する関数 327 328 Args: 329 observer_player (int): 観察者プレイヤーのインデックス 330 331 Returns: 332 dict: HLE形式の観察データ 333 """ 334 observation = {} 335 observation['current_player'] = self.current_player 336 observation['current_player_offset'] = self.get_current_player_offset(observer_player) 337 observation['deck_size'] = len(self.deck) 338 observation['discard_pile'] = [{'color': self.game_const.COLOR_NAMES[card[0]], 'rank': card[1]} for card in self.trash] 339 observation['fireworks'] = {self.game_const.COLOR_NAMES[i]: value for i, value in self.board.items()} 340 observation['information_tokens'] = self.hints 341 observation['legal_moves'] = [ self.action_to_hle_move(act) for act in self.valid_actions() ] 342 observation['life_tokens'] = self.miss 343 observation['observed_hands'] = self.get_observed_hands(observer_player) 344 observation['num_players'] = len(self.players) 345 observation['card_knowledge'] = self.get_card_knowledge() 346 return observation 347 348 def convert_hle_move_to_action(self, legal_move): 349 """ 350 HLEのアクションを `Action` クラスに変換する関数 351 352 Args: 353 legal_move (dict): HLE形式のアクション辞書 354 355 Returns: 356 Action: 変換後の `Action` オブジェクト 357 """ 358 """hle_moveをActionクラスに変換""" 359 if legal_move['action_type'] == 'PLAY': 360 return Action(self.game_const.PLAY, card_position=legal_move['card_index']) 361 elif legal_move['action_type'] == 'DISCARD': 362 return Action(self.game_const.DISCARD, card_position=legal_move['card_index']) 363 elif legal_move['action_type'] == 'REVEAL_COLOR': 364 target_player = (self.current_player + legal_move['target_offset']) % len(self.players) 365 return Action(self.game_const.HINT_COLOR, pnr=target_player, color=self.game_const.COLOR_NAMES.index(legal_move['color'])) 366 elif legal_move['action_type'] == 'REVEAL_RANK': 367 target_player = (self.current_player + legal_move['target_offset']) % len(self.players) 368 return Action(self.game_const.HINT_NUMBER, pnr=target_player, number=legal_move['rank']) 369 370################################################################################ 371 372 def is_current_player(self, player_idx): 373 """ 374 指定したプレイヤーが現在のプレイヤーかを判定する関数 375 376 Args: 377 player_idx (int): チェックするプレイヤーのインデックス 378 379 Returns: 380 bool: 現在のプレイヤーであればTrue,そうでなければFalse 381 """ 382 return self.current_player == player_idx 383 384 def random_perform(self): 385 """ 386 ランダムに有効なアクションを選択し実行する関数 387 """ 388 valid_actions = self.valid_actions() 389 if self.hints > 0: 390 action = random.choice([ action for action in valid_actions if (action.type == self.game_const.HINT_COLOR) or (action.type == self.game_const.HINT_NUMBER)]) 391 else: 392 action = random.choice([ action for action in valid_actions if action.type == self.game_const.DISCARD]) 393 self.perform(action=action.to_dict(), is_timeout=True) 394 395 def perform(self, action=None, is_timeout=False): 396 """ 397 指定されたアクションを実行する関数 398 399 Args: 400 action (dict, optional): 実行するアクション(デフォルトはNone) 401 is_timeout (bool, optional): タイムアウト発生時の処理かどうか(デフォルトはFalse) 402 """ 403 404 # アクションを取る前,状態データを記録 405 self.num_of_valid_actions = len(self.valid_actions()) 406 self.data_manager.set_turn_data_before_perform( 407 turn = self.turn+1, 408 hints = self.hints, 409 miss = self.miss, 410 board = self.board, 411 trash = self.trash, 412 deck = self.deck, 413 hands = self.hands, 414 game_const=self.game_const, 415 card_knowledge = self.get_card_knowledge(), 416 num_of_valid_actions = self.num_of_valid_actions, 417 score_per_turn = self.get_score() 418 ) 419 420 player = self.players[self.current_player] 421 if action is not None: 422 player.set_action(action) 423 424 # ここに一手の時間制限の処理 425 is_time_limit = is_timeout 426 427 # hleの変換 428 if player.need_hle_convert: 429 action = self.convert_hle_move_to_action(player.act(self.create_observation(self.current_player),self.random)) # HLE形式の場合,create_observationとrandインスタンスを引数に渡す 430 else: 431 action = player.act(self) # pyhanabi形式の場合,gameインスタンスを引数に渡す 432 433 self.turn_perform_time = self.get_now_unix() # アクション実行後の時間を記録 434 435 if action.type == self.game_const.PLAY: # カードをプレイするアクションの場合 436 action_type_txt,action_log = self.play_card(action) # カードをプレイ 437 elif action.type == self.game_const.DISCARD: # カードを捨てるアクションの場合 438 action_type_txt,action_log = self.discard_card(action) # カードを捨てる 439 elif action.type == self.game_const.HINT_COLOR or action.type == self.game_const.HINT_NUMBER: # ヒントを与えるアクションの場合 440 action_type_txt,action_log = self.give_hint(action) # ヒントを与える 441 442 # アクションを取った後,アクションのデータを記録 443 self.data_manager.set_turn_data_after_perform( 444 current_player = self.current_player, 445 action_type = action_type_txt, 446 action = action_log, 447 is_time_limit = is_time_limit, 448 turn_start_time = self.turn_start_time, 449 turn_perform_time = self.turn_perform_time, 450 ) 451 452 #self.print_log(self.data_manager.get_turn_data()) 453 self.next_turn() 454 455 def is_npc(self): 456 """ 457 現在のプレイヤーがNPC(WebsocketHumanAgent 以外)かを判定する関数 458 459 Returns: 460 bool: NPCであればTrue,そうでなければFalse 461 """ 462 return not isinstance(self.players[self.current_player], WebsocketHumanAgent) 463 464 def recover_hint(self): 465 """ヒントトークンを回復する処理 466 Note: 467 Hanabiは5のカードをプレイした際にヒントトークンを回復する. 468 """ 469 if self.hints < self.max_hints: # ヒントトークンが最大でない場合 470 self.hints += 1 # ヒントトークンを1回復 471 self.print_and_append_gui_log(f"5を出したのでヒントトークンが1つ回復しました.") 472 else: 473 self.print_and_append_gui_log(f"5を出しましたがすでに最大数です.回復は行われません.") 474 475 476 def play_card(self, action): 477 """ 478 カードをプレイする処理 479 480 Args: 481 action (Action): プレイヤーのアクション 482 483 Returns: 484 tuple: (アクションの種類, アクションのログ) 485 """ 486 card = self.hands[self.current_player][action.card_position] # プレイするカードを取得 487 card_color, card_number = card[0], card[1] # プレイしたカードの色と数字を取得 488 card_str = self.format_card(card) # カードをフォーマット 489 490 # プレイヤーがプレイしようとしているカードが正しい順序であるかを確認するif文 491 # self.board[card[0]]: ボード上のその色の現在の最大ランク 492 # self.board[card[0]] + 1: プレイが成功するために必要な次のランク 493 # card[1] == self.board[card[0]] + 1: 494 # - プレイヤーがプレイしようとしているカードの数字が、ボード上のその色の現在の最大ランクの次の数字であるかを確認 495 # - これにより、カードが正しい順序でプレイされているかを判定 496 if card_number == self.board[card_color] + 1: # プレイ成功の場合 497 self.board[card[0]] += 1 # ボード上のその色の現在の最大ランクを+1 498 499 player_name = self.players[self.current_player].name # プレイヤー名を取得 500 self.print_and_append_gui_log(f"P{self.current_player}:{action.card_position+1}番目をプレイ→{card_str}で成功") 501 502 # プレイ成功したカードが5の場合、ヒントトークンを回復する 503 if card_number == 5: 504 self.recover_hint() 505 506 action_log = {} 507 action_type_txt = 'PLAY' 508 action_log['action_type'] = action_type_txt 509 action_log['hand_index'] = action.card_position 510 action_log['card'] = card_str 511 action_log['is_success'] = True 512 else: 513 # プレイ失敗 514 self.trash.append(card) # カードを捨て山に追加 515 self.miss -= 1 # 赤トークンを1減らす 516 517 player_name = self.players[self.current_player].name # プレイヤー名を取得 518 self.print_and_append_gui_log(f"P{self.current_player}:{action.card_position+1}番目をプレイ→{card_str}で失敗") 519 520 action_log = {} 521 action_type_txt = 'PLAY' 522 action_log['action_type'] = action_type_txt 523 action_log['hand_index'] = action.card_position 524 action_log['card'] = card_str 525 action_log['is_success'] = False 526 527 528 # プレイ時の知識更新 529 self.knowledge_manager.update_knowledge_after_play(self.current_player,action.card_position) 530 531 # プレイ後、手札を更新 532 del self.hands[self.current_player][action.card_position] # プレイしたカードを手札から削除 533 self.draw_card(self.current_player) # 新しいカードを引く 534 535 return action_type_txt,action_log 536 537 def discard_card(self, action): 538 """ 539 カードを捨てる処理 540 541 Args: 542 action (Action): プレイヤーのアクション 543 544 Returns: 545 tuple: (アクションの種類, アクションのログ) 546 """ 547 card = self.hands[self.current_player][action.card_position] # 捨てるカードを取得 548 card_str = self.format_card(card) # カードをフォーマット 549 self.trash.append(card) # カードを捨て札に追加 550 551 player_name = self.players[self.current_player].name # プレイヤー名を取得 552 self.print_and_append_gui_log(f"P{self.current_player}:{action.card_position+1}番目を捨てる→{card_str}が捨てられた") 553 554 # 捨てるときの知識の更新 555 self.knowledge_manager.update_knowledge_after_discard(self.current_player,action.card_position) 556 557 # 捨てた後,手札を更新 558 del self.hands[self.current_player][action.card_position] # 捨てたカードを手札から削除 559 self.draw_card(self.current_player) # 新しいカードを引く 560 561 # ヒント数を1増やす(ただし,最大数まで) 562 self.hints = min(self.hints + 1, self.max_hints) 563 564 action_log = {} 565 action_type_txt = 'DISCARD' 566 action_log['action_type'] = action_type_txt 567 action_log['hand_index'] = action.card_position 568 action_log['card'] = card_str 569 return action_type_txt,action_log 570 571 def give_hint(self, action): 572 """ 573 ヒントを与える処理 574 575 Args: 576 action (Action): プレイヤーのアクション 577 578 Returns: 579 tuple: (アクションの種類, アクションのログ) 580 """ 581 player_name = self.players[self.current_player].name # プレイヤー名を取得 582 target_player = self.players[action.pnr].name # ヒントを与える相手の名前を取得 583 target_player_number = action.pnr # ヒントを与える相手の番号を取得 584 target_player_hand = self.hands[target_player_number] # ヒントを与える相手の手札を取得 585 586 if action.type == self.game_const.HINT_COLOR: 587 # 表示のためにヒントを与えるカードの位置を計算(左から何番目か) 588 color_cards_positions = [i for i, card in enumerate(target_player_hand) if card[0] == action.color] 589 positions_str = ','.join([f"{pos+1}" for pos in color_cards_positions]) 590 591 self.print_and_append_gui_log(f"P{self.current_player}→P{action.pnr}:手札の{positions_str}番目は{self.game_const.COLOR_NAMES[action.color]} 色です") 592 593 # 色ヒント時の知識更新 594 self.knowledge_manager.update_knowledge_after_color_hint(target_player_number, color_cards_positions, action.color) 595 596 action_log = {} 597 action_type_txt = 'REVEAL_COLOR' 598 action_log['action_type'] = action_type_txt 599 action_log['target_pid'] = target_player_number 600 action_log['target_hand_index'] = positions_str 601 action_log['color'] = self.game_const.COLOR_NAMES[action.color] 602 603 elif action.type == self.game_const.HINT_NUMBER: 604 # 表示のためにヒントを与えるカードの位置を計算(左から何番目か) 605 number_cards_positions = [i for i, card in enumerate(target_player_hand) if card[1] == action.number] 606 positions_str = ','.join([f"{pos+1}" for pos in number_cards_positions]) 607 608 self.print_and_append_gui_log(f"P{self.current_player}→P{action.pnr}:手札の{positions_str}番目の数字は{action.number}です") 609 610 # 数字ヒント時の知識更新 611 self.knowledge_manager.update_knowledge_after_number_hint(target_player_number, number_cards_positions, action.number) 612 613 action_log = {} 614 action_type_txt = 'REVEAL_RANK' 615 action_log['action_type'] = action_type_txt 616 action_log['target_pid'] = target_player_number 617 action_log['target_hand_index'] = positions_str 618 action_log['rank'] = action.number 619 620 self.hints -= 1 # ヒント数を1減らす 621 return action_type_txt,action_log 622 623 def valid_actions(self): 624 """現在のプレイヤーが取れる有効なアクションを返す 625 Returns: 626 valid_actions(list): 有効なアクションのリスト 627 """ 628 valid_actions = [] # 有効なアクションのリスト 629 val_memo = [] 630 631 # 自分の手札枚数×カードをプレイor捨てるアクションが有効 632 for i in range(len(self.hands[self.current_player])): # 自分の手札の枚数分繰り返す 633 valid_actions.append(Action(self.game_const.PLAY, card_position=i)) # カードをプレイするアクションを追加 634 val_memo.append(("PLAY",i)) 635 valid_actions.append(Action(self.game_const.DISCARD, card_position=i)) # カードを捨てるアクションを追加 636 val_memo.append(("DISC",i)) 637 638 # 青トークンが残っている場合,相手プレイヤーの持つ手札の色と数字に対してヒントを与えるアクションが有効 639 if self.hints > 0: # ヒントが残っている場合 640 for other_player in self.players: # 全プレイヤーぶん繰り返す 641 if other_player.player_number != self.current_player: # 自分以外のプレイヤーに対して 642 other_hand = self.hands[other_player.player_number] # 他のプレイヤーの手札を取得 643 644 existing_colors = {card[0] for card in other_hand} # 他のプレイヤーの手札に含まれる色の集合を取得 645 for color in existing_colors: # 他のプレイヤーの手札に含まれる色に対して 646 valid_actions.append(Action(self.game_const.HINT_COLOR, pnr=other_player.player_number, color=color)) # 色のヒントを与えるアクションを追加 647 val_memo.append(("HC",other_player.player_number,color)) 648 649 existing_numbers = {card[1] for card in other_hand} # 他のプレイヤーの手札に含まれる数字の集合を取得 650 for number in existing_numbers: # 他のプレイヤーの手札に含まれる数字に対して 651 valid_actions.append(Action(self.game_const.HINT_NUMBER, pnr=other_player.player_number, number=number))# 数字のヒントを与えるアクションを追加 652 val_memo.append(("HN",other_player.player_number,number)) 653 # self.print_log(f"val_memo:{val_memo}") 654 return valid_actions 655 656 def check_game_end(self): 657 """ゲームが終了する条件をチェックするメソッド 658 Returns: 659 bool: ゲームが終了しているかどうか.終了している場合はTrue, そうでない場合はFalse 660 """ 661 # 赤トークンが0になった場合、ゲーム終了 662 if self.miss == 0: 663 self.print_and_append_gui_log("ゲーム終了!ミスを3回してしまいました.") 664 self.game_end_reason = "3OUT" 665 return True 666 667 # 全ての色のカードが完成した場合、ゲーム終了 668 if all(rank == 5 for rank in self.board.values()): 669 self.print_and_append_gui_log("ゲーム終了!全てのカードが完成しました.") 670 self.game_end_reason = "PERFECT" 671 return True 672 673 # Extraターンが0かつ山札が空の場合、ゲーム終了 674 if self.extra_turns <= 0 and not self.deck: 675 self.print_and_append_gui_log("ゲーム終了!Extraターンが全て終了しました.") 676 self.game_end_reason = "END_EXTRA_TURNS" 677 return True 678 679 # まだゲームが終了していない場合 680 return False 681 682 683 def next_turn(self): 684 """次のターンに移る処理""" 685 self.turn += 1 # ターン数を1増やす 686 687 # エクストラターンが残っている場合はカウントを減らす 688 if self.extra_turns > 0: 689 self.extra_turns -= 1 690 self.print_and_append_gui_log(f"エクストラターン: 残り {self.extra_turns} ターン") 691 692 # 現在のプレイヤーを次のプレイヤーに変更する 693 # - 現在のプレイヤーのインデックスに1を加える 694 # - プレイヤーの数で割った余りを取ることで、プレイヤーのリストの範囲内にインデックスを収める 695 # - これにより、最後のプレイヤーの次は最初のプレイヤーに戻る 696 self.current_player = (self.current_player + 1) % len(self.players) 697 698 self.turn_start_time = self.get_now_unix() # ターン開始時間を更新 699 700 def format_card(self, card): 701 """カードの色とランクを色コード+数字でフォーマットする(表示用) 702 Args: 703 card(tuple): カードの色と数字のタプル 704 Returns: 705 str: カードの色コード+数字(例: B3) 706 """ 707 color_code = self.game_const.COLOR_NAMES[card[0]] # カードの色コード(例: B) 708 number = card[1] # カードの数字 709 return f"{color_code}{number}" # カードの色コード+数字(例: B3) 710 711 def print_game_state(self): 712 """ゲームの状態を文字列で表示する 713 Returns: 714 str: ゲームの状態を表す文字 715 """ 716 717 status = [] 718 status.append(f"--- Turn {self.turn+1} ---") 719 status.append("ボードに並んでいるカード:") 720 # ボードに並んでいるカードの色とランクを表示 721 for color, rank in self.board.items(): 722 status.append(f"{self.game_const.COLOR_NAMES[color]} : {rank}") # 例: B : 2 723 724 status.append(f"残りヒントの数: {self.hints}") 725 status.append(f"残り許されるミス回数: {self.miss}") 726 status.append(f"山札枚数: {len(self.deck)}") 727 728 status.append("捨て札内訳:") 729 if self.trash: 730 trash_dict = {color: [] for color in self.game_const.ALL_COLORS} # 各色の捨て札を保持する辞書 731 for card in self.trash: # 捨て札のカードを色ごとに分類 732 trash_dict[card[0]].append(card[1]) # カードの数字を追加 733 734 for color, numbers in trash_dict.items(): # 各色の捨て札を表示 735 if numbers: # その色の捨て札がある場合 736 numbers.sort() # 数字を昇順にソート 737 status.append(f"{self.game_const.COLOR_NAMES[color]}: {' '.join(map(str, numbers))}") # 例: B: 1 1 1 2 3 738 else: 739 status.append("捨て札なし") 740 741 status.append("プレイヤーの手札:") 742 # status.append(f"hands:{self.hands}") 743 for player in self.players: 744 hand = self.hands[player.player_number] 745 hand_str = ", ".join([self.format_card(card) for card in hand]) 746 status.append(f"プレイヤー {player.player_number} ({player.name}) の手札: {hand_str}") 747 748 # カード知識の表示をKnowledgeManagerの__str__メソッドから取得 749 # status.append(self.knowledge_manager.__str__()) 750 751 # for player_num in range(len(self.players)): 752 # observation = self.create_observation(player_num) 753 # status.append(f"--- Player {player_num}'s observation ---") 754 # for key, value in observation.items(): 755 # status.append(f"{key}: {value}") 756 757 self.print_log("\n".join(status))# appendされた各要素を改行で連結して返す 758 759 ########===================================================############# 760 # ここからrunnerのgame_runをCUI上でのシミュレーション用に移植&データ記録処理追加 761 762 def cui_game_run(self): 763 """ 764 CUI環境でゲームを実行する関数. 765 766 ゲーム終了条件を満たすまでループし,各ターンの状態を表示しながらアクションを実行する. 767 """ 768 if self.is_cui: 769 self.print_log("ゲームを開始します") 770 771 while not self.check_game_end(): 772 if self.is_cui: 773 self.print_game_state() 774 775 self.perform() # プレイヤーのアクションを処理 776 777 # whileを抜けたらゲーム終了 778 self.print_and_append_gui_log("ゲームが終了しました。") 779 self.print_and_append_gui_log(f"最終スコア: {self.get_score()}") 780 self.game_end() # ゲーム終了処理 781 782 def get_score(self): 783 """ 784 現在のスコアを取得する関数 785 786 Returns: 787 int: ゲームのスコア 788 """ 789 return sum(self.board.values()) 790 791 def game_end(self): 792 """ 793 ゲーム終了時の処理を行う関数 794 """ 795 796 self.game_end_time = self.get_now_unix() 797 798 #self.print_log(self.gui_log_history) 799 800 view_turn = self.turn+1 # ターン数は0から始まるため,+1する 801 self.data_manager.set_data_game_end( 802 turn = -1, 803 hints = self.hints, 804 miss = self.miss, 805 board = self.board, 806 trash = self.trash, 807 deck = self.deck, 808 hands = self.hands, 809 card_knowledge = self.get_card_knowledge(), 810 game_const=self.game_const, 811 num_of_valid_actions = self.num_of_valid_actions, 812 score_per_turn = self.get_score(), 813 final_score = self.get_score(), 814 final_turns=view_turn-1, 815 game_start_time = self.game_start_time, 816 game_end_time=self.game_end_time, 817 game_end_reason = self.game_end_reason 818 ) 819 820 ########===================================================############# 821 822 def print_and_append_gui_log(self, log): 823 """ 824 GUIログにメッセージを追加し,表示する関数. 825 826 Args: 827 log (str): ログメッセージ 828 """ 829 self.print_log(log) 830 self.gui_log_history.append(log) 831 832 def get_now_unix(self): 833 """ 834 現在のUnix時間を取得する関数 835 836 Returns: 837 int: 現在のUnix時間 838 """ 839 return int(datetime.datetime.now().timestamp()) 840 841 def timelimit_action(self): 842 """ 843 制限時間が切れた際に自動でアクションを決定する関数 844 845 Returns: 846 Action: 実行されるアクション 847 """ 848 # ヒントトークンが残っているなら,色か数字のヒントをランダムに与える 849 if self.hints > 0: 850 t = [self.game_const.HINT_COLOR, self.game_const.HINT_NUMBER] 851 hint_type = self.random.choice(t) # ヒントの種類をランダムに選択 852 pnr = self.random.choice([i for i in range(len(self.players)) if i != self.current_player]) # ヒントを出す相手をランダムに選択 853 854 if hint_type == self.game_const.HINT_COLOR: 855 color = self.random.choice([i for i in range(len(self.game_const.COLOR_NAMES))]) 856 return Action(self.game_const.HINT_COLOR, pnr=pnr, color=color) 857 else: 858 number = self.random.choice([i for i in range(5)]) 859 return Action(self.game_const.HINT_NUMBER, pnr=pnr, number=number) 860 861 else: 862 # ヒントトークンがない場合,手札からランダムにカードを捨てる 863 card_index = self.random.choice([i for i in range(len(self.hands[self.current_player]))]) 864 return Action(self.game_const.DISCARD, card_position=card_index)
23class Game: 24 """ゲーム全体の状態を管理するクラス 25 26 Attributes: 27 game_const(GameConst): ゲームの定数 28 players(list): 参加するプレイヤーのリスト 29 max_hints(int): ヒントトークンの最大数.(デフォルトは8個) 30 hints(int): 青トークンの数.hintを出せる回数としてhintsと記載 31 max_miss(int): 赤トークンの最大数.(デフォルトは3個) 32 miss(int): 赤トークンの数.プレイヤーがミスできる回数としてmissと記載. 33 board(dict): ボード上の各色のカードの最高ランク.初期状態として0を設定.最大5 34 trash(list): 捨てられたカードのリスト 35 deck(list): 山札 36 hands(dict): プレイヤーごとの手札.プレイヤー番号をキーとする辞書. 37 hand_size(int): プレイヤーの手札の枚数.2,3人プレイなら5枚,4,5人プレイなら4枚 38 current_player(int): 現在のプレイヤーを示す数字.プレイヤーのインデックスに対応. 39 ファーストプレイヤーを入れ替える場合,current_playerの初期値を変更するのではなく, 40 plauersリスト内で各プレイヤーに割り当てるindexを変更する形で対応すること 41 extra_turns(int): 残りエクストラターンの数. 42 山札切れの場合,エクストラターンに突入.extra_turnsがルール規定数分増え,0になるまで減りながらゲームが進行. 43 knowledge_manager(KnowledgeManager): カードの知識を管理するクラスのインスタンス 44 knowledge(list): カードの知識を格納した配列 45 """ 46 47 def __init__(self, players, turn_time_limit, seed=None, is_cui=True): 48 """ 49 ゲームの初期化 50 51 Args: 52 players (list): 参加するプレイヤーのリスト 53 turn_time_limit (int): 1ターンの制限時間(秒) 54 seed (int, optional): 乱数シード(デフォルトはNone) 55 is_cui (bool, optional): CUIモードで実行するか(デフォルトはTrue) 56 """ 57 self.seed = seed 58 self.random = random.Random(self.seed) 59 60 self.is_cui = is_cui # CUI表示を行うかどうかのフラグ 61 62 self.game_const = GameConst() 63 self.max_hints = 8 64 self.hints = self.max_hints 65 self.max_miss = 3 66 self.miss = self.max_miss 67 self.board = {color: 0 for color in self.game_const.ALL_COLORS} 68 self.trash = [] 69 self.deck = self.make_deck() 70 self.extra_turns = 0 71 self.turn = 0 72 self.current_player = 0 73 self.gui_log_history = [] # GUI表示用のアクション履歴 74 self.game_start_time = self.get_now_unix() # ゲーム開始時間 75 self.game_end_time = 0 # ゲーム終了時間 76 self.turn_time_limit = turn_time_limit # ターン制限時間(秒) 77 self.game_end_reason = "NOT_END" # ゲーム終了理由 78 self.num_of_valid_actions = 0 # 有効なアクション数 79 80 # 1ターンの時間計測用 81 self.turn_start_time = self.game_start_time # ターン開始時間(1ターン目はゲーム開始時間) 82 self.turn_perform_time = 0 # アクション実行後の時間 83 84 85 """プレイ人数に応じた初期化""" 86 self.players = players 87 for player in self.players: 88 player.set_game(self) 89 self.hands = {player.player_number: [] for player in players} 90 self.hand_size = 5 if len(players) in [2, 3] else 4 # 2,3人プレイなら5枚,4,5人プレイなら4枚 91 self.knowledge_manager = KnowledgeManager(self) 92 self.knowledge = self.knowledge_manager.initialize_all_knowledge() 93 self.make_hands() # 各プレイヤに初期手札を配る 94 95 # DataHandlerにゲーム開始時のデータをセット 96 self.data_manager = GameDataManager() 97 deck_str = "-".join([self.format_card(card) for card in self.deck]) 98 self.data_manager.set_data_game_start(players = self.players, turn_time_limit = self.turn_time_limit, deck = deck_str) 99 100 def print_log(self, log, arg=None): 101 """ 102 ログを出力する関数.CUIモードの場合にのみ表示する. 103 104 Args: 105 log (str): 出力するログメッセージ 106 arg (optional): 追加の引数(デフォルトはNone) 107 """ 108 if self.is_cui: 109 if arg is None: 110 print(log) 111 else: 112 log = log + ":" + str(arg) 113 print(log) 114 115 def make_deck(self): 116 """デッキを作成し、シャッフル 117 Returns: 118 deck(list): 山札 119 """ 120 deck = [] 121 for color in self.game_const.ALL_COLORS: # 全色に対して繰り返す 122 # 各色の規定カード枚数分繰り返す(デフォルト:1が3枚,2,3,4が2枚,5が1枚.) 123 for number, count in enumerate(self.game_const.COUNTS): 124 for _ in range(count): 125 deck.append((color, number + 1)) # 山札にカードを追加 126 # self.print_log("bf_deck",deck) 127 # self.print_log(self.random.random()) 128 self.random.shuffle(deck) # デッキをシャッフル 129 # self.print_log("af_deck",deck) 130 return deck 131 132 def make_hands(self): 133 """各プレイヤーに初期手札を配る""" 134 # for player in self.players: 135 # for _ in range(self.hand_size): # プレイヤー数に応じた手札枚数を配る 136 # card = self.deck.pop() # デッキからカードを引く 137 # self.hands[player.player_number].append(card) # 手札に追加 138 139 for player in self.players: 140 for _ in range(self.hand_size): # プレイヤー数に応じた手札枚数を配る 141 self.hands[player.player_number].append(self.deck[0]) # 手札に追加 142 del self.deck[0] 143 144 def draw_card(self, player_number): 145 """カードを引く 146 Args: 147 player_number(int): カードを引くプレイヤーの番号 148 """ 149 if len(self.deck) > 1: # デッキに2枚以上カードが残っている場合 150 self.hands[player_number].append(self.deck[0]) # 手札に追加 151 self.knowledge_manager.update_knowledge_after_draw(player_number) # カードを引いた後の知識更新 152 del self.deck[0] # デッキからカードを削除 153 elif len(self.deck) == 1: # デッキに残り1枚の場合 154 self.hands[player_number].append(self.deck[0]) # 手札に追加 155 self.knowledge_manager.update_knowledge_after_draw(player_number) # カードを引いた後の知識更新 156 del self.deck[0] # デッキからカードを削除 157 # 山札が0枚になったタイミングでエクストラターンを開始 158 self.print_and_append_gui_log("山札が切れました.") 159 self.print_and_append_gui_log("各プレイヤーに1ターンずつExtraターンを与えます.") 160 self.extra_turns = len(self.players)+1 # ゲーム進行処理上,エクストラターンはプレイヤー数+1回(next_turnで減算されるため) 161 else: 162 # デッキが既に空の場合は何もしない(エクストラターン中の可能性があるため,ここで終了処理しないこと) 163 pass 164 165################################################################################ 166# ここからhle_converter移植 167 def get_current_player_offset(self, observer_player): 168 """ 169 観察者から見た現在のプレイヤーまでのオフセットを計算する関数 170 171 Args: 172 observer_player (int): 観察者プレイヤーのインデックス 173 174 Returns: 175 int: 現在のプレイヤーまでのオフセット 176 """ 177 # 観察者から見た現在のプレイヤーまでのオフセットを計算 178 current_player_offset = (self.current_player - observer_player) % len(self.players) 179 return current_player_offset 180 181 def get_observed_hands(self, observer_player): 182 """ 183 指定したプレイヤーが観察できる手札を取得する関数 184 185 Args: 186 observer_player (int): 観察者プレイヤーのインデックス 187 188 Returns: 189 list: 観察者が見た手札のリスト(自身の手札の情報は非表示) 190 """ 191 observed_hands = [] 192 num_players = len(self.players) 193 194 for i in range(num_players): 195 player_index = (observer_player + i) % num_players # オフセットに基づき、順番を正しく取得 196 if player_index == observer_player: 197 # 観察者自身の手札は色とランクがわからないため、Noneと-1で表示 198 observed_hands.append([{'color': None, 'rank': -1} for _ in self.hands[player_index]]) 199 else: 200 # 他のプレイヤーの手札は実際のカード情報を返す 201 observed_hands.append([ 202 {'color': self.game_const.COLOR_NAMES[card[0]], 'rank': card[1]-1} # HLEはランクが0から始まるため-1 203 for card in self.hands[player_index] 204 ]) 205 return observed_hands 206 207 # 色または数字が確定しているかどうかを確認するヘルパーメソッド 208 def is_color_determined(self, card_knowledge): 209 """ 210 カードの色が確定しているかを判定する関数 211 212 Args: 213 card_knowledge (list): カードの知識情報 214 215 Returns: 216 str or None: 確定している場合は色名('B', 'G', 'R', 'W', 'Y'),未確定ならNone 217 """ 218 #self.print_log(f"card_knowledge:{card_knowledge}") 219 color_possibility = [] # BGRWY 220 221 for each_card_knowledge in card_knowledge: 222 #self.print_log(f"each_card_knowledge:{each_card_knowledge}") 223 #self.print_log(f"sum:{sum(each_card_knowledge)}") 224 # その色の知識が存在するならcolor_possibilityに1を追加,そうでないなら0を追加 225 if sum(each_card_knowledge) > 0: 226 color_possibility.append(1) 227 else: 228 color_possibility.append(0) 229 230 #self.print_log(color_possibility) 231 # color_possibilityの合計が1ならば、1色に確定しているので,その色を返す 232 if sum(color_possibility) == 1: 233 for j, item in enumerate(color_possibility): 234 if item == 1: 235 return self.game_const.COLOR_NAMES[j] 236 237 return None 238 239 def is_rank_determined(self, card_knowledge): 240 """ 241 カードのランクが確定しているかを判定する関数 242 243 Args: 244 card_knowledge (list): カードの知識情報 245 246 Returns: 247 int or None: 確定している場合はランク(0〜4),未確定ならNone 248 """ 249 #self.print_log(f"card_knowledge:{card_knowledge}") 250 number_possibility = [] # 01234 251 252 for num in range(5): 253 is_exist_num_knowledge = False 254 for each_card_knowledge in card_knowledge: 255 256 # numの知識が存在するなら1を追加 257 if each_card_knowledge[num] > 0: 258 is_exist_num_knowledge = True 259 260 if is_exist_num_knowledge: 261 number_possibility.append(1) 262 else: 263 number_possibility.append(0) 264 265 #self.print_log(number_possibility) 266 267 # number_possibilityの合計が1ならば、1つの数字に確定しているので,その数字を返す 268 if sum(number_possibility) == 1: 269 for j, item in enumerate(number_possibility): 270 if item == 1: 271 return j 272 273 274 def get_card_knowledge(self): 275 """ 276 各プレイヤーの手札に関する知識を取得する関数 277 278 Returns: 279 list: 各プレイヤーのカード知識を表すリスト 280 """ 281 card_knowledge = [] 282 for player_number in range(len(self.players)): 283 player_knowledge = [] 284 for each_card_knowledge in self.knowledge[player_number]: 285 #self.print_log(f"card:{each_card_knowledge}") 286 card_info = {'color': None, 'rank': None} 287 288 # 色の確定状況をチェック 289 card_info['color'] = self.is_color_determined(each_card_knowledge) 290 291 # ランクの確定状況をチェック 292 card_info['rank'] = self.is_rank_determined(each_card_knowledge) 293 294 player_knowledge.append(card_info) 295 card_knowledge.append(player_knowledge) 296 return card_knowledge 297 298 def action_to_hle_move(self, action): 299 """ 300 Hanabi Learning Environment(HLE)のアクション形式に変換する関数 301 302 Args: 303 action (Action): 変換するアクション 304 305 Returns: 306 dict: HLE形式のアクション辞書 307 """ 308 move = {} 309 if action.type == self.game_const.PLAY: 310 move['action_type'] = 'PLAY' 311 move['card_index'] = action.card_position 312 elif action.type == self.game_const.DISCARD: 313 move['action_type'] = 'DISCARD' 314 move['card_index'] = action.card_position 315 elif action.type == self.game_const.HINT_COLOR: 316 move['action_type'] = 'REVEAL_COLOR' 317 move['target_offset'] = self.get_current_player_offset(action.pnr) # プレイヤーのオフセットを計算 318 move['color'] = self.game_const.COLOR_NAMES[action.color] # ヒントの色 319 elif action.type == self.game_const.HINT_NUMBER: 320 move['action_type'] = 'REVEAL_RANK' 321 move['target_offset'] = self.get_current_player_offset(action.pnr) # プレイヤーのオフセットを計算 322 move['rank'] = action.number # ヒントの数字 323 return move 324 325 def create_observation(self, observer_player): 326 """ 327 指定したプレイヤーの観察データを作成する関数 328 329 Args: 330 observer_player (int): 観察者プレイヤーのインデックス 331 332 Returns: 333 dict: HLE形式の観察データ 334 """ 335 observation = {} 336 observation['current_player'] = self.current_player 337 observation['current_player_offset'] = self.get_current_player_offset(observer_player) 338 observation['deck_size'] = len(self.deck) 339 observation['discard_pile'] = [{'color': self.game_const.COLOR_NAMES[card[0]], 'rank': card[1]} for card in self.trash] 340 observation['fireworks'] = {self.game_const.COLOR_NAMES[i]: value for i, value in self.board.items()} 341 observation['information_tokens'] = self.hints 342 observation['legal_moves'] = [ self.action_to_hle_move(act) for act in self.valid_actions() ] 343 observation['life_tokens'] = self.miss 344 observation['observed_hands'] = self.get_observed_hands(observer_player) 345 observation['num_players'] = len(self.players) 346 observation['card_knowledge'] = self.get_card_knowledge() 347 return observation 348 349 def convert_hle_move_to_action(self, legal_move): 350 """ 351 HLEのアクションを `Action` クラスに変換する関数 352 353 Args: 354 legal_move (dict): HLE形式のアクション辞書 355 356 Returns: 357 Action: 変換後の `Action` オブジェクト 358 """ 359 """hle_moveをActionクラスに変換""" 360 if legal_move['action_type'] == 'PLAY': 361 return Action(self.game_const.PLAY, card_position=legal_move['card_index']) 362 elif legal_move['action_type'] == 'DISCARD': 363 return Action(self.game_const.DISCARD, card_position=legal_move['card_index']) 364 elif legal_move['action_type'] == 'REVEAL_COLOR': 365 target_player = (self.current_player + legal_move['target_offset']) % len(self.players) 366 return Action(self.game_const.HINT_COLOR, pnr=target_player, color=self.game_const.COLOR_NAMES.index(legal_move['color'])) 367 elif legal_move['action_type'] == 'REVEAL_RANK': 368 target_player = (self.current_player + legal_move['target_offset']) % len(self.players) 369 return Action(self.game_const.HINT_NUMBER, pnr=target_player, number=legal_move['rank']) 370 371################################################################################ 372 373 def is_current_player(self, player_idx): 374 """ 375 指定したプレイヤーが現在のプレイヤーかを判定する関数 376 377 Args: 378 player_idx (int): チェックするプレイヤーのインデックス 379 380 Returns: 381 bool: 現在のプレイヤーであればTrue,そうでなければFalse 382 """ 383 return self.current_player == player_idx 384 385 def random_perform(self): 386 """ 387 ランダムに有効なアクションを選択し実行する関数 388 """ 389 valid_actions = self.valid_actions() 390 if self.hints > 0: 391 action = random.choice([ action for action in valid_actions if (action.type == self.game_const.HINT_COLOR) or (action.type == self.game_const.HINT_NUMBER)]) 392 else: 393 action = random.choice([ action for action in valid_actions if action.type == self.game_const.DISCARD]) 394 self.perform(action=action.to_dict(), is_timeout=True) 395 396 def perform(self, action=None, is_timeout=False): 397 """ 398 指定されたアクションを実行する関数 399 400 Args: 401 action (dict, optional): 実行するアクション(デフォルトはNone) 402 is_timeout (bool, optional): タイムアウト発生時の処理かどうか(デフォルトはFalse) 403 """ 404 405 # アクションを取る前,状態データを記録 406 self.num_of_valid_actions = len(self.valid_actions()) 407 self.data_manager.set_turn_data_before_perform( 408 turn = self.turn+1, 409 hints = self.hints, 410 miss = self.miss, 411 board = self.board, 412 trash = self.trash, 413 deck = self.deck, 414 hands = self.hands, 415 game_const=self.game_const, 416 card_knowledge = self.get_card_knowledge(), 417 num_of_valid_actions = self.num_of_valid_actions, 418 score_per_turn = self.get_score() 419 ) 420 421 player = self.players[self.current_player] 422 if action is not None: 423 player.set_action(action) 424 425 # ここに一手の時間制限の処理 426 is_time_limit = is_timeout 427 428 # hleの変換 429 if player.need_hle_convert: 430 action = self.convert_hle_move_to_action(player.act(self.create_observation(self.current_player),self.random)) # HLE形式の場合,create_observationとrandインスタンスを引数に渡す 431 else: 432 action = player.act(self) # pyhanabi形式の場合,gameインスタンスを引数に渡す 433 434 self.turn_perform_time = self.get_now_unix() # アクション実行後の時間を記録 435 436 if action.type == self.game_const.PLAY: # カードをプレイするアクションの場合 437 action_type_txt,action_log = self.play_card(action) # カードをプレイ 438 elif action.type == self.game_const.DISCARD: # カードを捨てるアクションの場合 439 action_type_txt,action_log = self.discard_card(action) # カードを捨てる 440 elif action.type == self.game_const.HINT_COLOR or action.type == self.game_const.HINT_NUMBER: # ヒントを与えるアクションの場合 441 action_type_txt,action_log = self.give_hint(action) # ヒントを与える 442 443 # アクションを取った後,アクションのデータを記録 444 self.data_manager.set_turn_data_after_perform( 445 current_player = self.current_player, 446 action_type = action_type_txt, 447 action = action_log, 448 is_time_limit = is_time_limit, 449 turn_start_time = self.turn_start_time, 450 turn_perform_time = self.turn_perform_time, 451 ) 452 453 #self.print_log(self.data_manager.get_turn_data()) 454 self.next_turn() 455 456 def is_npc(self): 457 """ 458 現在のプレイヤーがNPC(WebsocketHumanAgent 以外)かを判定する関数 459 460 Returns: 461 bool: NPCであればTrue,そうでなければFalse 462 """ 463 return not isinstance(self.players[self.current_player], WebsocketHumanAgent) 464 465 def recover_hint(self): 466 """ヒントトークンを回復する処理 467 Note: 468 Hanabiは5のカードをプレイした際にヒントトークンを回復する. 469 """ 470 if self.hints < self.max_hints: # ヒントトークンが最大でない場合 471 self.hints += 1 # ヒントトークンを1回復 472 self.print_and_append_gui_log(f"5を出したのでヒントトークンが1つ回復しました.") 473 else: 474 self.print_and_append_gui_log(f"5を出しましたがすでに最大数です.回復は行われません.") 475 476 477 def play_card(self, action): 478 """ 479 カードをプレイする処理 480 481 Args: 482 action (Action): プレイヤーのアクション 483 484 Returns: 485 tuple: (アクションの種類, アクションのログ) 486 """ 487 card = self.hands[self.current_player][action.card_position] # プレイするカードを取得 488 card_color, card_number = card[0], card[1] # プレイしたカードの色と数字を取得 489 card_str = self.format_card(card) # カードをフォーマット 490 491 # プレイヤーがプレイしようとしているカードが正しい順序であるかを確認するif文 492 # self.board[card[0]]: ボード上のその色の現在の最大ランク 493 # self.board[card[0]] + 1: プレイが成功するために必要な次のランク 494 # card[1] == self.board[card[0]] + 1: 495 # - プレイヤーがプレイしようとしているカードの数字が、ボード上のその色の現在の最大ランクの次の数字であるかを確認 496 # - これにより、カードが正しい順序でプレイされているかを判定 497 if card_number == self.board[card_color] + 1: # プレイ成功の場合 498 self.board[card[0]] += 1 # ボード上のその色の現在の最大ランクを+1 499 500 player_name = self.players[self.current_player].name # プレイヤー名を取得 501 self.print_and_append_gui_log(f"P{self.current_player}:{action.card_position+1}番目をプレイ→{card_str}で成功") 502 503 # プレイ成功したカードが5の場合、ヒントトークンを回復する 504 if card_number == 5: 505 self.recover_hint() 506 507 action_log = {} 508 action_type_txt = 'PLAY' 509 action_log['action_type'] = action_type_txt 510 action_log['hand_index'] = action.card_position 511 action_log['card'] = card_str 512 action_log['is_success'] = True 513 else: 514 # プレイ失敗 515 self.trash.append(card) # カードを捨て山に追加 516 self.miss -= 1 # 赤トークンを1減らす 517 518 player_name = self.players[self.current_player].name # プレイヤー名を取得 519 self.print_and_append_gui_log(f"P{self.current_player}:{action.card_position+1}番目をプレイ→{card_str}で失敗") 520 521 action_log = {} 522 action_type_txt = 'PLAY' 523 action_log['action_type'] = action_type_txt 524 action_log['hand_index'] = action.card_position 525 action_log['card'] = card_str 526 action_log['is_success'] = False 527 528 529 # プレイ時の知識更新 530 self.knowledge_manager.update_knowledge_after_play(self.current_player,action.card_position) 531 532 # プレイ後、手札を更新 533 del self.hands[self.current_player][action.card_position] # プレイしたカードを手札から削除 534 self.draw_card(self.current_player) # 新しいカードを引く 535 536 return action_type_txt,action_log 537 538 def discard_card(self, action): 539 """ 540 カードを捨てる処理 541 542 Args: 543 action (Action): プレイヤーのアクション 544 545 Returns: 546 tuple: (アクションの種類, アクションのログ) 547 """ 548 card = self.hands[self.current_player][action.card_position] # 捨てるカードを取得 549 card_str = self.format_card(card) # カードをフォーマット 550 self.trash.append(card) # カードを捨て札に追加 551 552 player_name = self.players[self.current_player].name # プレイヤー名を取得 553 self.print_and_append_gui_log(f"P{self.current_player}:{action.card_position+1}番目を捨てる→{card_str}が捨てられた") 554 555 # 捨てるときの知識の更新 556 self.knowledge_manager.update_knowledge_after_discard(self.current_player,action.card_position) 557 558 # 捨てた後,手札を更新 559 del self.hands[self.current_player][action.card_position] # 捨てたカードを手札から削除 560 self.draw_card(self.current_player) # 新しいカードを引く 561 562 # ヒント数を1増やす(ただし,最大数まで) 563 self.hints = min(self.hints + 1, self.max_hints) 564 565 action_log = {} 566 action_type_txt = 'DISCARD' 567 action_log['action_type'] = action_type_txt 568 action_log['hand_index'] = action.card_position 569 action_log['card'] = card_str 570 return action_type_txt,action_log 571 572 def give_hint(self, action): 573 """ 574 ヒントを与える処理 575 576 Args: 577 action (Action): プレイヤーのアクション 578 579 Returns: 580 tuple: (アクションの種類, アクションのログ) 581 """ 582 player_name = self.players[self.current_player].name # プレイヤー名を取得 583 target_player = self.players[action.pnr].name # ヒントを与える相手の名前を取得 584 target_player_number = action.pnr # ヒントを与える相手の番号を取得 585 target_player_hand = self.hands[target_player_number] # ヒントを与える相手の手札を取得 586 587 if action.type == self.game_const.HINT_COLOR: 588 # 表示のためにヒントを与えるカードの位置を計算(左から何番目か) 589 color_cards_positions = [i for i, card in enumerate(target_player_hand) if card[0] == action.color] 590 positions_str = ','.join([f"{pos+1}" for pos in color_cards_positions]) 591 592 self.print_and_append_gui_log(f"P{self.current_player}→P{action.pnr}:手札の{positions_str}番目は{self.game_const.COLOR_NAMES[action.color]} 色です") 593 594 # 色ヒント時の知識更新 595 self.knowledge_manager.update_knowledge_after_color_hint(target_player_number, color_cards_positions, action.color) 596 597 action_log = {} 598 action_type_txt = 'REVEAL_COLOR' 599 action_log['action_type'] = action_type_txt 600 action_log['target_pid'] = target_player_number 601 action_log['target_hand_index'] = positions_str 602 action_log['color'] = self.game_const.COLOR_NAMES[action.color] 603 604 elif action.type == self.game_const.HINT_NUMBER: 605 # 表示のためにヒントを与えるカードの位置を計算(左から何番目か) 606 number_cards_positions = [i for i, card in enumerate(target_player_hand) if card[1] == action.number] 607 positions_str = ','.join([f"{pos+1}" for pos in number_cards_positions]) 608 609 self.print_and_append_gui_log(f"P{self.current_player}→P{action.pnr}:手札の{positions_str}番目の数字は{action.number}です") 610 611 # 数字ヒント時の知識更新 612 self.knowledge_manager.update_knowledge_after_number_hint(target_player_number, number_cards_positions, action.number) 613 614 action_log = {} 615 action_type_txt = 'REVEAL_RANK' 616 action_log['action_type'] = action_type_txt 617 action_log['target_pid'] = target_player_number 618 action_log['target_hand_index'] = positions_str 619 action_log['rank'] = action.number 620 621 self.hints -= 1 # ヒント数を1減らす 622 return action_type_txt,action_log 623 624 def valid_actions(self): 625 """現在のプレイヤーが取れる有効なアクションを返す 626 Returns: 627 valid_actions(list): 有効なアクションのリスト 628 """ 629 valid_actions = [] # 有効なアクションのリスト 630 val_memo = [] 631 632 # 自分の手札枚数×カードをプレイor捨てるアクションが有効 633 for i in range(len(self.hands[self.current_player])): # 自分の手札の枚数分繰り返す 634 valid_actions.append(Action(self.game_const.PLAY, card_position=i)) # カードをプレイするアクションを追加 635 val_memo.append(("PLAY",i)) 636 valid_actions.append(Action(self.game_const.DISCARD, card_position=i)) # カードを捨てるアクションを追加 637 val_memo.append(("DISC",i)) 638 639 # 青トークンが残っている場合,相手プレイヤーの持つ手札の色と数字に対してヒントを与えるアクションが有効 640 if self.hints > 0: # ヒントが残っている場合 641 for other_player in self.players: # 全プレイヤーぶん繰り返す 642 if other_player.player_number != self.current_player: # 自分以外のプレイヤーに対して 643 other_hand = self.hands[other_player.player_number] # 他のプレイヤーの手札を取得 644 645 existing_colors = {card[0] for card in other_hand} # 他のプレイヤーの手札に含まれる色の集合を取得 646 for color in existing_colors: # 他のプレイヤーの手札に含まれる色に対して 647 valid_actions.append(Action(self.game_const.HINT_COLOR, pnr=other_player.player_number, color=color)) # 色のヒントを与えるアクションを追加 648 val_memo.append(("HC",other_player.player_number,color)) 649 650 existing_numbers = {card[1] for card in other_hand} # 他のプレイヤーの手札に含まれる数字の集合を取得 651 for number in existing_numbers: # 他のプレイヤーの手札に含まれる数字に対して 652 valid_actions.append(Action(self.game_const.HINT_NUMBER, pnr=other_player.player_number, number=number))# 数字のヒントを与えるアクションを追加 653 val_memo.append(("HN",other_player.player_number,number)) 654 # self.print_log(f"val_memo:{val_memo}") 655 return valid_actions 656 657 def check_game_end(self): 658 """ゲームが終了する条件をチェックするメソッド 659 Returns: 660 bool: ゲームが終了しているかどうか.終了している場合はTrue, そうでない場合はFalse 661 """ 662 # 赤トークンが0になった場合、ゲーム終了 663 if self.miss == 0: 664 self.print_and_append_gui_log("ゲーム終了!ミスを3回してしまいました.") 665 self.game_end_reason = "3OUT" 666 return True 667 668 # 全ての色のカードが完成した場合、ゲーム終了 669 if all(rank == 5 for rank in self.board.values()): 670 self.print_and_append_gui_log("ゲーム終了!全てのカードが完成しました.") 671 self.game_end_reason = "PERFECT" 672 return True 673 674 # Extraターンが0かつ山札が空の場合、ゲーム終了 675 if self.extra_turns <= 0 and not self.deck: 676 self.print_and_append_gui_log("ゲーム終了!Extraターンが全て終了しました.") 677 self.game_end_reason = "END_EXTRA_TURNS" 678 return True 679 680 # まだゲームが終了していない場合 681 return False 682 683 684 def next_turn(self): 685 """次のターンに移る処理""" 686 self.turn += 1 # ターン数を1増やす 687 688 # エクストラターンが残っている場合はカウントを減らす 689 if self.extra_turns > 0: 690 self.extra_turns -= 1 691 self.print_and_append_gui_log(f"エクストラターン: 残り {self.extra_turns} ターン") 692 693 # 現在のプレイヤーを次のプレイヤーに変更する 694 # - 現在のプレイヤーのインデックスに1を加える 695 # - プレイヤーの数で割った余りを取ることで、プレイヤーのリストの範囲内にインデックスを収める 696 # - これにより、最後のプレイヤーの次は最初のプレイヤーに戻る 697 self.current_player = (self.current_player + 1) % len(self.players) 698 699 self.turn_start_time = self.get_now_unix() # ターン開始時間を更新 700 701 def format_card(self, card): 702 """カードの色とランクを色コード+数字でフォーマットする(表示用) 703 Args: 704 card(tuple): カードの色と数字のタプル 705 Returns: 706 str: カードの色コード+数字(例: B3) 707 """ 708 color_code = self.game_const.COLOR_NAMES[card[0]] # カードの色コード(例: B) 709 number = card[1] # カードの数字 710 return f"{color_code}{number}" # カードの色コード+数字(例: B3) 711 712 def print_game_state(self): 713 """ゲームの状態を文字列で表示する 714 Returns: 715 str: ゲームの状態を表す文字 716 """ 717 718 status = [] 719 status.append(f"--- Turn {self.turn+1} ---") 720 status.append("ボードに並んでいるカード:") 721 # ボードに並んでいるカードの色とランクを表示 722 for color, rank in self.board.items(): 723 status.append(f"{self.game_const.COLOR_NAMES[color]} : {rank}") # 例: B : 2 724 725 status.append(f"残りヒントの数: {self.hints}") 726 status.append(f"残り許されるミス回数: {self.miss}") 727 status.append(f"山札枚数: {len(self.deck)}") 728 729 status.append("捨て札内訳:") 730 if self.trash: 731 trash_dict = {color: [] for color in self.game_const.ALL_COLORS} # 各色の捨て札を保持する辞書 732 for card in self.trash: # 捨て札のカードを色ごとに分類 733 trash_dict[card[0]].append(card[1]) # カードの数字を追加 734 735 for color, numbers in trash_dict.items(): # 各色の捨て札を表示 736 if numbers: # その色の捨て札がある場合 737 numbers.sort() # 数字を昇順にソート 738 status.append(f"{self.game_const.COLOR_NAMES[color]}: {' '.join(map(str, numbers))}") # 例: B: 1 1 1 2 3 739 else: 740 status.append("捨て札なし") 741 742 status.append("プレイヤーの手札:") 743 # status.append(f"hands:{self.hands}") 744 for player in self.players: 745 hand = self.hands[player.player_number] 746 hand_str = ", ".join([self.format_card(card) for card in hand]) 747 status.append(f"プレイヤー {player.player_number} ({player.name}) の手札: {hand_str}") 748 749 # カード知識の表示をKnowledgeManagerの__str__メソッドから取得 750 # status.append(self.knowledge_manager.__str__()) 751 752 # for player_num in range(len(self.players)): 753 # observation = self.create_observation(player_num) 754 # status.append(f"--- Player {player_num}'s observation ---") 755 # for key, value in observation.items(): 756 # status.append(f"{key}: {value}") 757 758 self.print_log("\n".join(status))# appendされた各要素を改行で連結して返す 759 760 ########===================================================############# 761 # ここからrunnerのgame_runをCUI上でのシミュレーション用に移植&データ記録処理追加 762 763 def cui_game_run(self): 764 """ 765 CUI環境でゲームを実行する関数. 766 767 ゲーム終了条件を満たすまでループし,各ターンの状態を表示しながらアクションを実行する. 768 """ 769 if self.is_cui: 770 self.print_log("ゲームを開始します") 771 772 while not self.check_game_end(): 773 if self.is_cui: 774 self.print_game_state() 775 776 self.perform() # プレイヤーのアクションを処理 777 778 # whileを抜けたらゲーム終了 779 self.print_and_append_gui_log("ゲームが終了しました。") 780 self.print_and_append_gui_log(f"最終スコア: {self.get_score()}") 781 self.game_end() # ゲーム終了処理 782 783 def get_score(self): 784 """ 785 現在のスコアを取得する関数 786 787 Returns: 788 int: ゲームのスコア 789 """ 790 return sum(self.board.values()) 791 792 def game_end(self): 793 """ 794 ゲーム終了時の処理を行う関数 795 """ 796 797 self.game_end_time = self.get_now_unix() 798 799 #self.print_log(self.gui_log_history) 800 801 view_turn = self.turn+1 # ターン数は0から始まるため,+1する 802 self.data_manager.set_data_game_end( 803 turn = -1, 804 hints = self.hints, 805 miss = self.miss, 806 board = self.board, 807 trash = self.trash, 808 deck = self.deck, 809 hands = self.hands, 810 card_knowledge = self.get_card_knowledge(), 811 game_const=self.game_const, 812 num_of_valid_actions = self.num_of_valid_actions, 813 score_per_turn = self.get_score(), 814 final_score = self.get_score(), 815 final_turns=view_turn-1, 816 game_start_time = self.game_start_time, 817 game_end_time=self.game_end_time, 818 game_end_reason = self.game_end_reason 819 ) 820 821 ########===================================================############# 822 823 def print_and_append_gui_log(self, log): 824 """ 825 GUIログにメッセージを追加し,表示する関数. 826 827 Args: 828 log (str): ログメッセージ 829 """ 830 self.print_log(log) 831 self.gui_log_history.append(log) 832 833 def get_now_unix(self): 834 """ 835 現在のUnix時間を取得する関数 836 837 Returns: 838 int: 現在のUnix時間 839 """ 840 return int(datetime.datetime.now().timestamp()) 841 842 def timelimit_action(self): 843 """ 844 制限時間が切れた際に自動でアクションを決定する関数 845 846 Returns: 847 Action: 実行されるアクション 848 """ 849 # ヒントトークンが残っているなら,色か数字のヒントをランダムに与える 850 if self.hints > 0: 851 t = [self.game_const.HINT_COLOR, self.game_const.HINT_NUMBER] 852 hint_type = self.random.choice(t) # ヒントの種類をランダムに選択 853 pnr = self.random.choice([i for i in range(len(self.players)) if i != self.current_player]) # ヒントを出す相手をランダムに選択 854 855 if hint_type == self.game_const.HINT_COLOR: 856 color = self.random.choice([i for i in range(len(self.game_const.COLOR_NAMES))]) 857 return Action(self.game_const.HINT_COLOR, pnr=pnr, color=color) 858 else: 859 number = self.random.choice([i for i in range(5)]) 860 return Action(self.game_const.HINT_NUMBER, pnr=pnr, number=number) 861 862 else: 863 # ヒントトークンがない場合,手札からランダムにカードを捨てる 864 card_index = self.random.choice([i for i in range(len(self.hands[self.current_player]))]) 865 return Action(self.game_const.DISCARD, card_position=card_index)
ゲーム全体の状態を管理するクラス
Attributes:
- game_const(GameConst): ゲームの定数
- players(list): 参加するプレイヤーのリスト
- max_hints(int): ヒントトークンの最大数.(デフォルトは8個)
- hints(int): 青トークンの数.hintを出せる回数としてhintsと記載
- max_miss(int): 赤トークンの最大数.(デフォルトは3個)
- miss(int): 赤トークンの数.プレイヤーがミスできる回数としてmissと記載.
- board(dict): ボード上の各色のカードの最高ランク.初期状態として0を設定.最大5
- trash(list): 捨てられたカードのリスト
- deck(list): 山札
- hands(dict): プレイヤーごとの手札.プレイヤー番号をキーとする辞書.
- hand_size(int): プレイヤーの手札の枚数.2,3人プレイなら5枚,4,5人プレイなら4枚
- current_player(int): 現在のプレイヤーを示す数字.プレイヤーのインデックスに対応. ファーストプレイヤーを入れ替える場合,current_playerの初期値を変更するのではなく, plauersリスト内で各プレイヤーに割り当てるindexを変更する形で対応すること
- extra_turns(int): 残りエクストラターンの数. 山札切れの場合,エクストラターンに突入.extra_turnsがルール規定数分増え,0になるまで減りながらゲームが進行.
- knowledge_manager(KnowledgeManager): カードの知識を管理するクラスのインスタンス
- knowledge(list): カードの知識を格納した配列
47 def __init__(self, players, turn_time_limit, seed=None, is_cui=True): 48 """ 49 ゲームの初期化 50 51 Args: 52 players (list): 参加するプレイヤーのリスト 53 turn_time_limit (int): 1ターンの制限時間(秒) 54 seed (int, optional): 乱数シード(デフォルトはNone) 55 is_cui (bool, optional): CUIモードで実行するか(デフォルトはTrue) 56 """ 57 self.seed = seed 58 self.random = random.Random(self.seed) 59 60 self.is_cui = is_cui # CUI表示を行うかどうかのフラグ 61 62 self.game_const = GameConst() 63 self.max_hints = 8 64 self.hints = self.max_hints 65 self.max_miss = 3 66 self.miss = self.max_miss 67 self.board = {color: 0 for color in self.game_const.ALL_COLORS} 68 self.trash = [] 69 self.deck = self.make_deck() 70 self.extra_turns = 0 71 self.turn = 0 72 self.current_player = 0 73 self.gui_log_history = [] # GUI表示用のアクション履歴 74 self.game_start_time = self.get_now_unix() # ゲーム開始時間 75 self.game_end_time = 0 # ゲーム終了時間 76 self.turn_time_limit = turn_time_limit # ターン制限時間(秒) 77 self.game_end_reason = "NOT_END" # ゲーム終了理由 78 self.num_of_valid_actions = 0 # 有効なアクション数 79 80 # 1ターンの時間計測用 81 self.turn_start_time = self.game_start_time # ターン開始時間(1ターン目はゲーム開始時間) 82 self.turn_perform_time = 0 # アクション実行後の時間 83 84 85 """プレイ人数に応じた初期化""" 86 self.players = players 87 for player in self.players: 88 player.set_game(self) 89 self.hands = {player.player_number: [] for player in players} 90 self.hand_size = 5 if len(players) in [2, 3] else 4 # 2,3人プレイなら5枚,4,5人プレイなら4枚 91 self.knowledge_manager = KnowledgeManager(self) 92 self.knowledge = self.knowledge_manager.initialize_all_knowledge() 93 self.make_hands() # 各プレイヤに初期手札を配る 94 95 # DataHandlerにゲーム開始時のデータをセット 96 self.data_manager = GameDataManager() 97 deck_str = "-".join([self.format_card(card) for card in self.deck]) 98 self.data_manager.set_data_game_start(players = self.players, turn_time_limit = self.turn_time_limit, deck = deck_str)
ゲームの初期化
Arguments:
- players (list): 参加するプレイヤーのリスト
- turn_time_limit (int): 1ターンの制限時間(秒)
- seed (int, optional): 乱数シード(デフォルトはNone)
- is_cui (bool, optional): CUIモードで実行するか(デフォルトはTrue)
100 def print_log(self, log, arg=None): 101 """ 102 ログを出力する関数.CUIモードの場合にのみ表示する. 103 104 Args: 105 log (str): 出力するログメッセージ 106 arg (optional): 追加の引数(デフォルトはNone) 107 """ 108 if self.is_cui: 109 if arg is None: 110 print(log) 111 else: 112 log = log + ":" + str(arg) 113 print(log)
ログを出力する関数.CUIモードの場合にのみ表示する.
Arguments:
- log (str): 出力するログメッセージ
- arg (optional): 追加の引数(デフォルトはNone)
115 def make_deck(self): 116 """デッキを作成し、シャッフル 117 Returns: 118 deck(list): 山札 119 """ 120 deck = [] 121 for color in self.game_const.ALL_COLORS: # 全色に対して繰り返す 122 # 各色の規定カード枚数分繰り返す(デフォルト:1が3枚,2,3,4が2枚,5が1枚.) 123 for number, count in enumerate(self.game_const.COUNTS): 124 for _ in range(count): 125 deck.append((color, number + 1)) # 山札にカードを追加 126 # self.print_log("bf_deck",deck) 127 # self.print_log(self.random.random()) 128 self.random.shuffle(deck) # デッキをシャッフル 129 # self.print_log("af_deck",deck) 130 return deck
デッキを作成し、シャッフル
Returns:
deck(list): 山札
132 def make_hands(self): 133 """各プレイヤーに初期手札を配る""" 134 # for player in self.players: 135 # for _ in range(self.hand_size): # プレイヤー数に応じた手札枚数を配る 136 # card = self.deck.pop() # デッキからカードを引く 137 # self.hands[player.player_number].append(card) # 手札に追加 138 139 for player in self.players: 140 for _ in range(self.hand_size): # プレイヤー数に応じた手札枚数を配る 141 self.hands[player.player_number].append(self.deck[0]) # 手札に追加 142 del self.deck[0]
各プレイヤーに初期手札を配る
144 def draw_card(self, player_number): 145 """カードを引く 146 Args: 147 player_number(int): カードを引くプレイヤーの番号 148 """ 149 if len(self.deck) > 1: # デッキに2枚以上カードが残っている場合 150 self.hands[player_number].append(self.deck[0]) # 手札に追加 151 self.knowledge_manager.update_knowledge_after_draw(player_number) # カードを引いた後の知識更新 152 del self.deck[0] # デッキからカードを削除 153 elif len(self.deck) == 1: # デッキに残り1枚の場合 154 self.hands[player_number].append(self.deck[0]) # 手札に追加 155 self.knowledge_manager.update_knowledge_after_draw(player_number) # カードを引いた後の知識更新 156 del self.deck[0] # デッキからカードを削除 157 # 山札が0枚になったタイミングでエクストラターンを開始 158 self.print_and_append_gui_log("山札が切れました.") 159 self.print_and_append_gui_log("各プレイヤーに1ターンずつExtraターンを与えます.") 160 self.extra_turns = len(self.players)+1 # ゲーム進行処理上,エクストラターンはプレイヤー数+1回(next_turnで減算されるため) 161 else: 162 # デッキが既に空の場合は何もしない(エクストラターン中の可能性があるため,ここで終了処理しないこと) 163 pass
カードを引く
Arguments:
- player_number(int): カードを引くプレイヤーの番号
167 def get_current_player_offset(self, observer_player): 168 """ 169 観察者から見た現在のプレイヤーまでのオフセットを計算する関数 170 171 Args: 172 observer_player (int): 観察者プレイヤーのインデックス 173 174 Returns: 175 int: 現在のプレイヤーまでのオフセット 176 """ 177 # 観察者から見た現在のプレイヤーまでのオフセットを計算 178 current_player_offset = (self.current_player - observer_player) % len(self.players) 179 return current_player_offset
観察者から見た現在のプレイヤーまでのオフセットを計算する関数
Arguments:
- observer_player (int): 観察者プレイヤーのインデックス
Returns:
int: 現在のプレイヤーまでのオフセット
181 def get_observed_hands(self, observer_player): 182 """ 183 指定したプレイヤーが観察できる手札を取得する関数 184 185 Args: 186 observer_player (int): 観察者プレイヤーのインデックス 187 188 Returns: 189 list: 観察者が見た手札のリスト(自身の手札の情報は非表示) 190 """ 191 observed_hands = [] 192 num_players = len(self.players) 193 194 for i in range(num_players): 195 player_index = (observer_player + i) % num_players # オフセットに基づき、順番を正しく取得 196 if player_index == observer_player: 197 # 観察者自身の手札は色とランクがわからないため、Noneと-1で表示 198 observed_hands.append([{'color': None, 'rank': -1} for _ in self.hands[player_index]]) 199 else: 200 # 他のプレイヤーの手札は実際のカード情報を返す 201 observed_hands.append([ 202 {'color': self.game_const.COLOR_NAMES[card[0]], 'rank': card[1]-1} # HLEはランクが0から始まるため-1 203 for card in self.hands[player_index] 204 ]) 205 return observed_hands
指定したプレイヤーが観察できる手札を取得する関数
Arguments:
- observer_player (int): 観察者プレイヤーのインデックス
Returns:
list: 観察者が見た手札のリスト(自身の手札の情報は非表示)
208 def is_color_determined(self, card_knowledge): 209 """ 210 カードの色が確定しているかを判定する関数 211 212 Args: 213 card_knowledge (list): カードの知識情報 214 215 Returns: 216 str or None: 確定している場合は色名('B', 'G', 'R', 'W', 'Y'),未確定ならNone 217 """ 218 #self.print_log(f"card_knowledge:{card_knowledge}") 219 color_possibility = [] # BGRWY 220 221 for each_card_knowledge in card_knowledge: 222 #self.print_log(f"each_card_knowledge:{each_card_knowledge}") 223 #self.print_log(f"sum:{sum(each_card_knowledge)}") 224 # その色の知識が存在するならcolor_possibilityに1を追加,そうでないなら0を追加 225 if sum(each_card_knowledge) > 0: 226 color_possibility.append(1) 227 else: 228 color_possibility.append(0) 229 230 #self.print_log(color_possibility) 231 # color_possibilityの合計が1ならば、1色に確定しているので,その色を返す 232 if sum(color_possibility) == 1: 233 for j, item in enumerate(color_possibility): 234 if item == 1: 235 return self.game_const.COLOR_NAMES[j] 236 237 return None
カードの色が確定しているかを判定する関数
Arguments:
- card_knowledge (list): カードの知識情報
Returns:
str or None: 確定している場合は色名('B', 'G', 'R', 'W', 'Y'),未確定ならNone
239 def is_rank_determined(self, card_knowledge): 240 """ 241 カードのランクが確定しているかを判定する関数 242 243 Args: 244 card_knowledge (list): カードの知識情報 245 246 Returns: 247 int or None: 確定している場合はランク(0〜4),未確定ならNone 248 """ 249 #self.print_log(f"card_knowledge:{card_knowledge}") 250 number_possibility = [] # 01234 251 252 for num in range(5): 253 is_exist_num_knowledge = False 254 for each_card_knowledge in card_knowledge: 255 256 # numの知識が存在するなら1を追加 257 if each_card_knowledge[num] > 0: 258 is_exist_num_knowledge = True 259 260 if is_exist_num_knowledge: 261 number_possibility.append(1) 262 else: 263 number_possibility.append(0) 264 265 #self.print_log(number_possibility) 266 267 # number_possibilityの合計が1ならば、1つの数字に確定しているので,その数字を返す 268 if sum(number_possibility) == 1: 269 for j, item in enumerate(number_possibility): 270 if item == 1: 271 return j
カードのランクが確定しているかを判定する関数
Arguments:
- card_knowledge (list): カードの知識情報
Returns:
int or None: 確定している場合はランク(0〜4),未確定ならNone
274 def get_card_knowledge(self): 275 """ 276 各プレイヤーの手札に関する知識を取得する関数 277 278 Returns: 279 list: 各プレイヤーのカード知識を表すリスト 280 """ 281 card_knowledge = [] 282 for player_number in range(len(self.players)): 283 player_knowledge = [] 284 for each_card_knowledge in self.knowledge[player_number]: 285 #self.print_log(f"card:{each_card_knowledge}") 286 card_info = {'color': None, 'rank': None} 287 288 # 色の確定状況をチェック 289 card_info['color'] = self.is_color_determined(each_card_knowledge) 290 291 # ランクの確定状況をチェック 292 card_info['rank'] = self.is_rank_determined(each_card_knowledge) 293 294 player_knowledge.append(card_info) 295 card_knowledge.append(player_knowledge) 296 return card_knowledge
各プレイヤーの手札に関する知識を取得する関数
Returns:
list: 各プレイヤーのカード知識を表すリスト
298 def action_to_hle_move(self, action): 299 """ 300 Hanabi Learning Environment(HLE)のアクション形式に変換する関数 301 302 Args: 303 action (Action): 変換するアクション 304 305 Returns: 306 dict: HLE形式のアクション辞書 307 """ 308 move = {} 309 if action.type == self.game_const.PLAY: 310 move['action_type'] = 'PLAY' 311 move['card_index'] = action.card_position 312 elif action.type == self.game_const.DISCARD: 313 move['action_type'] = 'DISCARD' 314 move['card_index'] = action.card_position 315 elif action.type == self.game_const.HINT_COLOR: 316 move['action_type'] = 'REVEAL_COLOR' 317 move['target_offset'] = self.get_current_player_offset(action.pnr) # プレイヤーのオフセットを計算 318 move['color'] = self.game_const.COLOR_NAMES[action.color] # ヒントの色 319 elif action.type == self.game_const.HINT_NUMBER: 320 move['action_type'] = 'REVEAL_RANK' 321 move['target_offset'] = self.get_current_player_offset(action.pnr) # プレイヤーのオフセットを計算 322 move['rank'] = action.number # ヒントの数字 323 return move
Hanabi Learning Environment(HLE)のアクション形式に変換する関数
Arguments:
- action (Action): 変換するアクション
Returns:
dict: HLE形式のアクション辞書
325 def create_observation(self, observer_player): 326 """ 327 指定したプレイヤーの観察データを作成する関数 328 329 Args: 330 observer_player (int): 観察者プレイヤーのインデックス 331 332 Returns: 333 dict: HLE形式の観察データ 334 """ 335 observation = {} 336 observation['current_player'] = self.current_player 337 observation['current_player_offset'] = self.get_current_player_offset(observer_player) 338 observation['deck_size'] = len(self.deck) 339 observation['discard_pile'] = [{'color': self.game_const.COLOR_NAMES[card[0]], 'rank': card[1]} for card in self.trash] 340 observation['fireworks'] = {self.game_const.COLOR_NAMES[i]: value for i, value in self.board.items()} 341 observation['information_tokens'] = self.hints 342 observation['legal_moves'] = [ self.action_to_hle_move(act) for act in self.valid_actions() ] 343 observation['life_tokens'] = self.miss 344 observation['observed_hands'] = self.get_observed_hands(observer_player) 345 observation['num_players'] = len(self.players) 346 observation['card_knowledge'] = self.get_card_knowledge() 347 return observation
指定したプレイヤーの観察データを作成する関数
Arguments:
- observer_player (int): 観察者プレイヤーのインデックス
Returns:
dict: HLE形式の観察データ
349 def convert_hle_move_to_action(self, legal_move): 350 """ 351 HLEのアクションを `Action` クラスに変換する関数 352 353 Args: 354 legal_move (dict): HLE形式のアクション辞書 355 356 Returns: 357 Action: 変換後の `Action` オブジェクト 358 """ 359 """hle_moveをActionクラスに変換""" 360 if legal_move['action_type'] == 'PLAY': 361 return Action(self.game_const.PLAY, card_position=legal_move['card_index']) 362 elif legal_move['action_type'] == 'DISCARD': 363 return Action(self.game_const.DISCARD, card_position=legal_move['card_index']) 364 elif legal_move['action_type'] == 'REVEAL_COLOR': 365 target_player = (self.current_player + legal_move['target_offset']) % len(self.players) 366 return Action(self.game_const.HINT_COLOR, pnr=target_player, color=self.game_const.COLOR_NAMES.index(legal_move['color'])) 367 elif legal_move['action_type'] == 'REVEAL_RANK': 368 target_player = (self.current_player + legal_move['target_offset']) % len(self.players) 369 return Action(self.game_const.HINT_NUMBER, pnr=target_player, number=legal_move['rank'])
HLEのアクションを Action
クラスに変換する関数
Arguments:
- legal_move (dict): HLE形式のアクション辞書
Returns:
Action: 変換後の
Action
オブジェクト
373 def is_current_player(self, player_idx): 374 """ 375 指定したプレイヤーが現在のプレイヤーかを判定する関数 376 377 Args: 378 player_idx (int): チェックするプレイヤーのインデックス 379 380 Returns: 381 bool: 現在のプレイヤーであればTrue,そうでなければFalse 382 """ 383 return self.current_player == player_idx
指定したプレイヤーが現在のプレイヤーかを判定する関数
Arguments:
- player_idx (int): チェックするプレイヤーのインデックス
Returns:
bool: 現在のプレイヤーであればTrue,そうでなければFalse
385 def random_perform(self): 386 """ 387 ランダムに有効なアクションを選択し実行する関数 388 """ 389 valid_actions = self.valid_actions() 390 if self.hints > 0: 391 action = random.choice([ action for action in valid_actions if (action.type == self.game_const.HINT_COLOR) or (action.type == self.game_const.HINT_NUMBER)]) 392 else: 393 action = random.choice([ action for action in valid_actions if action.type == self.game_const.DISCARD]) 394 self.perform(action=action.to_dict(), is_timeout=True)
ランダムに有効なアクションを選択し実行する関数
396 def perform(self, action=None, is_timeout=False): 397 """ 398 指定されたアクションを実行する関数 399 400 Args: 401 action (dict, optional): 実行するアクション(デフォルトはNone) 402 is_timeout (bool, optional): タイムアウト発生時の処理かどうか(デフォルトはFalse) 403 """ 404 405 # アクションを取る前,状態データを記録 406 self.num_of_valid_actions = len(self.valid_actions()) 407 self.data_manager.set_turn_data_before_perform( 408 turn = self.turn+1, 409 hints = self.hints, 410 miss = self.miss, 411 board = self.board, 412 trash = self.trash, 413 deck = self.deck, 414 hands = self.hands, 415 game_const=self.game_const, 416 card_knowledge = self.get_card_knowledge(), 417 num_of_valid_actions = self.num_of_valid_actions, 418 score_per_turn = self.get_score() 419 ) 420 421 player = self.players[self.current_player] 422 if action is not None: 423 player.set_action(action) 424 425 # ここに一手の時間制限の処理 426 is_time_limit = is_timeout 427 428 # hleの変換 429 if player.need_hle_convert: 430 action = self.convert_hle_move_to_action(player.act(self.create_observation(self.current_player),self.random)) # HLE形式の場合,create_observationとrandインスタンスを引数に渡す 431 else: 432 action = player.act(self) # pyhanabi形式の場合,gameインスタンスを引数に渡す 433 434 self.turn_perform_time = self.get_now_unix() # アクション実行後の時間を記録 435 436 if action.type == self.game_const.PLAY: # カードをプレイするアクションの場合 437 action_type_txt,action_log = self.play_card(action) # カードをプレイ 438 elif action.type == self.game_const.DISCARD: # カードを捨てるアクションの場合 439 action_type_txt,action_log = self.discard_card(action) # カードを捨てる 440 elif action.type == self.game_const.HINT_COLOR or action.type == self.game_const.HINT_NUMBER: # ヒントを与えるアクションの場合 441 action_type_txt,action_log = self.give_hint(action) # ヒントを与える 442 443 # アクションを取った後,アクションのデータを記録 444 self.data_manager.set_turn_data_after_perform( 445 current_player = self.current_player, 446 action_type = action_type_txt, 447 action = action_log, 448 is_time_limit = is_time_limit, 449 turn_start_time = self.turn_start_time, 450 turn_perform_time = self.turn_perform_time, 451 ) 452 453 #self.print_log(self.data_manager.get_turn_data()) 454 self.next_turn()
指定されたアクションを実行する関数
Arguments:
- action (dict, optional): 実行するアクション(デフォルトはNone)
- is_timeout (bool, optional): タイムアウト発生時の処理かどうか(デフォルトはFalse)
456 def is_npc(self): 457 """ 458 現在のプレイヤーがNPC(WebsocketHumanAgent 以外)かを判定する関数 459 460 Returns: 461 bool: NPCであればTrue,そうでなければFalse 462 """ 463 return not isinstance(self.players[self.current_player], WebsocketHumanAgent)
現在のプレイヤーがNPC(WebsocketHumanAgent 以外)かを判定する関数
Returns:
bool: NPCであればTrue,そうでなければFalse
465 def recover_hint(self): 466 """ヒントトークンを回復する処理 467 Note: 468 Hanabiは5のカードをプレイした際にヒントトークンを回復する. 469 """ 470 if self.hints < self.max_hints: # ヒントトークンが最大でない場合 471 self.hints += 1 # ヒントトークンを1回復 472 self.print_and_append_gui_log(f"5を出したのでヒントトークンが1つ回復しました.") 473 else: 474 self.print_and_append_gui_log(f"5を出しましたがすでに最大数です.回復は行われません.")
ヒントトークンを回復する処理
Note:
Hanabiは5のカードをプレイした際にヒントトークンを回復する.
477 def play_card(self, action): 478 """ 479 カードをプレイする処理 480 481 Args: 482 action (Action): プレイヤーのアクション 483 484 Returns: 485 tuple: (アクションの種類, アクションのログ) 486 """ 487 card = self.hands[self.current_player][action.card_position] # プレイするカードを取得 488 card_color, card_number = card[0], card[1] # プレイしたカードの色と数字を取得 489 card_str = self.format_card(card) # カードをフォーマット 490 491 # プレイヤーがプレイしようとしているカードが正しい順序であるかを確認するif文 492 # self.board[card[0]]: ボード上のその色の現在の最大ランク 493 # self.board[card[0]] + 1: プレイが成功するために必要な次のランク 494 # card[1] == self.board[card[0]] + 1: 495 # - プレイヤーがプレイしようとしているカードの数字が、ボード上のその色の現在の最大ランクの次の数字であるかを確認 496 # - これにより、カードが正しい順序でプレイされているかを判定 497 if card_number == self.board[card_color] + 1: # プレイ成功の場合 498 self.board[card[0]] += 1 # ボード上のその色の現在の最大ランクを+1 499 500 player_name = self.players[self.current_player].name # プレイヤー名を取得 501 self.print_and_append_gui_log(f"P{self.current_player}:{action.card_position+1}番目をプレイ→{card_str}で成功") 502 503 # プレイ成功したカードが5の場合、ヒントトークンを回復する 504 if card_number == 5: 505 self.recover_hint() 506 507 action_log = {} 508 action_type_txt = 'PLAY' 509 action_log['action_type'] = action_type_txt 510 action_log['hand_index'] = action.card_position 511 action_log['card'] = card_str 512 action_log['is_success'] = True 513 else: 514 # プレイ失敗 515 self.trash.append(card) # カードを捨て山に追加 516 self.miss -= 1 # 赤トークンを1減らす 517 518 player_name = self.players[self.current_player].name # プレイヤー名を取得 519 self.print_and_append_gui_log(f"P{self.current_player}:{action.card_position+1}番目をプレイ→{card_str}で失敗") 520 521 action_log = {} 522 action_type_txt = 'PLAY' 523 action_log['action_type'] = action_type_txt 524 action_log['hand_index'] = action.card_position 525 action_log['card'] = card_str 526 action_log['is_success'] = False 527 528 529 # プレイ時の知識更新 530 self.knowledge_manager.update_knowledge_after_play(self.current_player,action.card_position) 531 532 # プレイ後、手札を更新 533 del self.hands[self.current_player][action.card_position] # プレイしたカードを手札から削除 534 self.draw_card(self.current_player) # 新しいカードを引く 535 536 return action_type_txt,action_log
カードをプレイする処理
Arguments:
- action (Action): プレイヤーのアクション
Returns:
tuple: (アクションの種類, アクションのログ)
538 def discard_card(self, action): 539 """ 540 カードを捨てる処理 541 542 Args: 543 action (Action): プレイヤーのアクション 544 545 Returns: 546 tuple: (アクションの種類, アクションのログ) 547 """ 548 card = self.hands[self.current_player][action.card_position] # 捨てるカードを取得 549 card_str = self.format_card(card) # カードをフォーマット 550 self.trash.append(card) # カードを捨て札に追加 551 552 player_name = self.players[self.current_player].name # プレイヤー名を取得 553 self.print_and_append_gui_log(f"P{self.current_player}:{action.card_position+1}番目を捨てる→{card_str}が捨てられた") 554 555 # 捨てるときの知識の更新 556 self.knowledge_manager.update_knowledge_after_discard(self.current_player,action.card_position) 557 558 # 捨てた後,手札を更新 559 del self.hands[self.current_player][action.card_position] # 捨てたカードを手札から削除 560 self.draw_card(self.current_player) # 新しいカードを引く 561 562 # ヒント数を1増やす(ただし,最大数まで) 563 self.hints = min(self.hints + 1, self.max_hints) 564 565 action_log = {} 566 action_type_txt = 'DISCARD' 567 action_log['action_type'] = action_type_txt 568 action_log['hand_index'] = action.card_position 569 action_log['card'] = card_str 570 return action_type_txt,action_log
カードを捨てる処理
Arguments:
- action (Action): プレイヤーのアクション
Returns:
tuple: (アクションの種類, アクションのログ)
572 def give_hint(self, action): 573 """ 574 ヒントを与える処理 575 576 Args: 577 action (Action): プレイヤーのアクション 578 579 Returns: 580 tuple: (アクションの種類, アクションのログ) 581 """ 582 player_name = self.players[self.current_player].name # プレイヤー名を取得 583 target_player = self.players[action.pnr].name # ヒントを与える相手の名前を取得 584 target_player_number = action.pnr # ヒントを与える相手の番号を取得 585 target_player_hand = self.hands[target_player_number] # ヒントを与える相手の手札を取得 586 587 if action.type == self.game_const.HINT_COLOR: 588 # 表示のためにヒントを与えるカードの位置を計算(左から何番目か) 589 color_cards_positions = [i for i, card in enumerate(target_player_hand) if card[0] == action.color] 590 positions_str = ','.join([f"{pos+1}" for pos in color_cards_positions]) 591 592 self.print_and_append_gui_log(f"P{self.current_player}→P{action.pnr}:手札の{positions_str}番目は{self.game_const.COLOR_NAMES[action.color]} 色です") 593 594 # 色ヒント時の知識更新 595 self.knowledge_manager.update_knowledge_after_color_hint(target_player_number, color_cards_positions, action.color) 596 597 action_log = {} 598 action_type_txt = 'REVEAL_COLOR' 599 action_log['action_type'] = action_type_txt 600 action_log['target_pid'] = target_player_number 601 action_log['target_hand_index'] = positions_str 602 action_log['color'] = self.game_const.COLOR_NAMES[action.color] 603 604 elif action.type == self.game_const.HINT_NUMBER: 605 # 表示のためにヒントを与えるカードの位置を計算(左から何番目か) 606 number_cards_positions = [i for i, card in enumerate(target_player_hand) if card[1] == action.number] 607 positions_str = ','.join([f"{pos+1}" for pos in number_cards_positions]) 608 609 self.print_and_append_gui_log(f"P{self.current_player}→P{action.pnr}:手札の{positions_str}番目の数字は{action.number}です") 610 611 # 数字ヒント時の知識更新 612 self.knowledge_manager.update_knowledge_after_number_hint(target_player_number, number_cards_positions, action.number) 613 614 action_log = {} 615 action_type_txt = 'REVEAL_RANK' 616 action_log['action_type'] = action_type_txt 617 action_log['target_pid'] = target_player_number 618 action_log['target_hand_index'] = positions_str 619 action_log['rank'] = action.number 620 621 self.hints -= 1 # ヒント数を1減らす 622 return action_type_txt,action_log
ヒントを与える処理
Arguments:
- action (Action): プレイヤーのアクション
Returns:
tuple: (アクションの種類, アクションのログ)
624 def valid_actions(self): 625 """現在のプレイヤーが取れる有効なアクションを返す 626 Returns: 627 valid_actions(list): 有効なアクションのリスト 628 """ 629 valid_actions = [] # 有効なアクションのリスト 630 val_memo = [] 631 632 # 自分の手札枚数×カードをプレイor捨てるアクションが有効 633 for i in range(len(self.hands[self.current_player])): # 自分の手札の枚数分繰り返す 634 valid_actions.append(Action(self.game_const.PLAY, card_position=i)) # カードをプレイするアクションを追加 635 val_memo.append(("PLAY",i)) 636 valid_actions.append(Action(self.game_const.DISCARD, card_position=i)) # カードを捨てるアクションを追加 637 val_memo.append(("DISC",i)) 638 639 # 青トークンが残っている場合,相手プレイヤーの持つ手札の色と数字に対してヒントを与えるアクションが有効 640 if self.hints > 0: # ヒントが残っている場合 641 for other_player in self.players: # 全プレイヤーぶん繰り返す 642 if other_player.player_number != self.current_player: # 自分以外のプレイヤーに対して 643 other_hand = self.hands[other_player.player_number] # 他のプレイヤーの手札を取得 644 645 existing_colors = {card[0] for card in other_hand} # 他のプレイヤーの手札に含まれる色の集合を取得 646 for color in existing_colors: # 他のプレイヤーの手札に含まれる色に対して 647 valid_actions.append(Action(self.game_const.HINT_COLOR, pnr=other_player.player_number, color=color)) # 色のヒントを与えるアクションを追加 648 val_memo.append(("HC",other_player.player_number,color)) 649 650 existing_numbers = {card[1] for card in other_hand} # 他のプレイヤーの手札に含まれる数字の集合を取得 651 for number in existing_numbers: # 他のプレイヤーの手札に含まれる数字に対して 652 valid_actions.append(Action(self.game_const.HINT_NUMBER, pnr=other_player.player_number, number=number))# 数字のヒントを与えるアクションを追加 653 val_memo.append(("HN",other_player.player_number,number)) 654 # self.print_log(f"val_memo:{val_memo}") 655 return valid_actions
現在のプレイヤーが取れる有効なアクションを返す
Returns:
valid_actions(list): 有効なアクションのリスト
657 def check_game_end(self): 658 """ゲームが終了する条件をチェックするメソッド 659 Returns: 660 bool: ゲームが終了しているかどうか.終了している場合はTrue, そうでない場合はFalse 661 """ 662 # 赤トークンが0になった場合、ゲーム終了 663 if self.miss == 0: 664 self.print_and_append_gui_log("ゲーム終了!ミスを3回してしまいました.") 665 self.game_end_reason = "3OUT" 666 return True 667 668 # 全ての色のカードが完成した場合、ゲーム終了 669 if all(rank == 5 for rank in self.board.values()): 670 self.print_and_append_gui_log("ゲーム終了!全てのカードが完成しました.") 671 self.game_end_reason = "PERFECT" 672 return True 673 674 # Extraターンが0かつ山札が空の場合、ゲーム終了 675 if self.extra_turns <= 0 and not self.deck: 676 self.print_and_append_gui_log("ゲーム終了!Extraターンが全て終了しました.") 677 self.game_end_reason = "END_EXTRA_TURNS" 678 return True 679 680 # まだゲームが終了していない場合 681 return False
ゲームが終了する条件をチェックするメソッド
Returns:
bool: ゲームが終了しているかどうか.終了している場合はTrue, そうでない場合はFalse
684 def next_turn(self): 685 """次のターンに移る処理""" 686 self.turn += 1 # ターン数を1増やす 687 688 # エクストラターンが残っている場合はカウントを減らす 689 if self.extra_turns > 0: 690 self.extra_turns -= 1 691 self.print_and_append_gui_log(f"エクストラターン: 残り {self.extra_turns} ターン") 692 693 # 現在のプレイヤーを次のプレイヤーに変更する 694 # - 現在のプレイヤーのインデックスに1を加える 695 # - プレイヤーの数で割った余りを取ることで、プレイヤーのリストの範囲内にインデックスを収める 696 # - これにより、最後のプレイヤーの次は最初のプレイヤーに戻る 697 self.current_player = (self.current_player + 1) % len(self.players) 698 699 self.turn_start_time = self.get_now_unix() # ターン開始時間を更新
次のターンに移る処理
701 def format_card(self, card): 702 """カードの色とランクを色コード+数字でフォーマットする(表示用) 703 Args: 704 card(tuple): カードの色と数字のタプル 705 Returns: 706 str: カードの色コード+数字(例: B3) 707 """ 708 color_code = self.game_const.COLOR_NAMES[card[0]] # カードの色コード(例: B) 709 number = card[1] # カードの数字 710 return f"{color_code}{number}" # カードの色コード+数字(例: B3)
カードの色とランクを色コード+数字でフォーマットする(表示用)
Arguments:
- card(tuple): カードの色と数字のタプル
Returns:
str: カードの色コード+数字(例: B3)
712 def print_game_state(self): 713 """ゲームの状態を文字列で表示する 714 Returns: 715 str: ゲームの状態を表す文字 716 """ 717 718 status = [] 719 status.append(f"--- Turn {self.turn+1} ---") 720 status.append("ボードに並んでいるカード:") 721 # ボードに並んでいるカードの色とランクを表示 722 for color, rank in self.board.items(): 723 status.append(f"{self.game_const.COLOR_NAMES[color]} : {rank}") # 例: B : 2 724 725 status.append(f"残りヒントの数: {self.hints}") 726 status.append(f"残り許されるミス回数: {self.miss}") 727 status.append(f"山札枚数: {len(self.deck)}") 728 729 status.append("捨て札内訳:") 730 if self.trash: 731 trash_dict = {color: [] for color in self.game_const.ALL_COLORS} # 各色の捨て札を保持する辞書 732 for card in self.trash: # 捨て札のカードを色ごとに分類 733 trash_dict[card[0]].append(card[1]) # カードの数字を追加 734 735 for color, numbers in trash_dict.items(): # 各色の捨て札を表示 736 if numbers: # その色の捨て札がある場合 737 numbers.sort() # 数字を昇順にソート 738 status.append(f"{self.game_const.COLOR_NAMES[color]}: {' '.join(map(str, numbers))}") # 例: B: 1 1 1 2 3 739 else: 740 status.append("捨て札なし") 741 742 status.append("プレイヤーの手札:") 743 # status.append(f"hands:{self.hands}") 744 for player in self.players: 745 hand = self.hands[player.player_number] 746 hand_str = ", ".join([self.format_card(card) for card in hand]) 747 status.append(f"プレイヤー {player.player_number} ({player.name}) の手札: {hand_str}") 748 749 # カード知識の表示をKnowledgeManagerの__str__メソッドから取得 750 # status.append(self.knowledge_manager.__str__()) 751 752 # for player_num in range(len(self.players)): 753 # observation = self.create_observation(player_num) 754 # status.append(f"--- Player {player_num}'s observation ---") 755 # for key, value in observation.items(): 756 # status.append(f"{key}: {value}") 757 758 self.print_log("\n".join(status))# appendされた各要素を改行で連結して返す
ゲームの状態を文字列で表示する
Returns:
str: ゲームの状態を表す文字
763 def cui_game_run(self): 764 """ 765 CUI環境でゲームを実行する関数. 766 767 ゲーム終了条件を満たすまでループし,各ターンの状態を表示しながらアクションを実行する. 768 """ 769 if self.is_cui: 770 self.print_log("ゲームを開始します") 771 772 while not self.check_game_end(): 773 if self.is_cui: 774 self.print_game_state() 775 776 self.perform() # プレイヤーのアクションを処理 777 778 # whileを抜けたらゲーム終了 779 self.print_and_append_gui_log("ゲームが終了しました。") 780 self.print_and_append_gui_log(f"最終スコア: {self.get_score()}") 781 self.game_end() # ゲーム終了処理
CUI環境でゲームを実行する関数.
ゲーム終了条件を満たすまでループし,各ターンの状態を表示しながらアクションを実行する.
783 def get_score(self): 784 """ 785 現在のスコアを取得する関数 786 787 Returns: 788 int: ゲームのスコア 789 """ 790 return sum(self.board.values())
現在のスコアを取得する関数
Returns:
int: ゲームのスコア
792 def game_end(self): 793 """ 794 ゲーム終了時の処理を行う関数 795 """ 796 797 self.game_end_time = self.get_now_unix() 798 799 #self.print_log(self.gui_log_history) 800 801 view_turn = self.turn+1 # ターン数は0から始まるため,+1する 802 self.data_manager.set_data_game_end( 803 turn = -1, 804 hints = self.hints, 805 miss = self.miss, 806 board = self.board, 807 trash = self.trash, 808 deck = self.deck, 809 hands = self.hands, 810 card_knowledge = self.get_card_knowledge(), 811 game_const=self.game_const, 812 num_of_valid_actions = self.num_of_valid_actions, 813 score_per_turn = self.get_score(), 814 final_score = self.get_score(), 815 final_turns=view_turn-1, 816 game_start_time = self.game_start_time, 817 game_end_time=self.game_end_time, 818 game_end_reason = self.game_end_reason 819 )
ゲーム終了時の処理を行う関数
823 def print_and_append_gui_log(self, log): 824 """ 825 GUIログにメッセージを追加し,表示する関数. 826 827 Args: 828 log (str): ログメッセージ 829 """ 830 self.print_log(log) 831 self.gui_log_history.append(log)
GUIログにメッセージを追加し,表示する関数.
Arguments:
- log (str): ログメッセージ
833 def get_now_unix(self): 834 """ 835 現在のUnix時間を取得する関数 836 837 Returns: 838 int: 現在のUnix時間 839 """ 840 return int(datetime.datetime.now().timestamp())
現在のUnix時間を取得する関数
Returns:
int: 現在のUnix時間
842 def timelimit_action(self): 843 """ 844 制限時間が切れた際に自動でアクションを決定する関数 845 846 Returns: 847 Action: 実行されるアクション 848 """ 849 # ヒントトークンが残っているなら,色か数字のヒントをランダムに与える 850 if self.hints > 0: 851 t = [self.game_const.HINT_COLOR, self.game_const.HINT_NUMBER] 852 hint_type = self.random.choice(t) # ヒントの種類をランダムに選択 853 pnr = self.random.choice([i for i in range(len(self.players)) if i != self.current_player]) # ヒントを出す相手をランダムに選択 854 855 if hint_type == self.game_const.HINT_COLOR: 856 color = self.random.choice([i for i in range(len(self.game_const.COLOR_NAMES))]) 857 return Action(self.game_const.HINT_COLOR, pnr=pnr, color=color) 858 else: 859 number = self.random.choice([i for i in range(5)]) 860 return Action(self.game_const.HINT_NUMBER, pnr=pnr, number=number) 861 862 else: 863 # ヒントトークンがない場合,手札からランダムにカードを捨てる 864 card_index = self.random.choice([i for i in range(len(self.hands[self.current_player]))]) 865 return Action(self.game_const.DISCARD, card_position=card_index)
制限時間が切れた際に自動でアクションを決定する関数
Returns:
Action: 実行されるアクション