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)
class Game:
 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): カードの知識を格納した配列
Game(players, turn_time_limit, seed=None, is_cui=True)
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)
seed
random
is_cui
game_const
max_hints
hints
max_miss
miss
board
trash
deck
extra_turns
turn
current_player
gui_log_history
game_start_time
game_end_time
turn_time_limit
game_end_reason
num_of_valid_actions
turn_start_time
turn_perform_time

プレイ人数に応じた初期化

players
hands
hand_size
knowledge_manager
knowledge
data_manager
def print_log(self, log, arg=None):
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)
def make_deck(self):
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): 山札

def make_hands(self):
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]

各プレイヤーに初期手札を配る

def draw_card(self, player_number):
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): カードを引くプレイヤーの番号
def get_current_player_offset(self, observer_player):
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: 現在のプレイヤーまでのオフセット

def get_observed_hands(self, observer_player):
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: 観察者が見た手札のリスト(自身の手札の情報は非表示)

def is_color_determined(self, card_knowledge):
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

def is_rank_determined(self, card_knowledge):
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

def get_card_knowledge(self):
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: 各プレイヤーのカード知識を表すリスト

def action_to_hle_move(self, action):
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形式のアクション辞書

def create_observation(self, observer_player):
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形式の観察データ

def convert_hle_move_to_action(self, legal_move):
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 オブジェクト

def is_current_player(self, player_idx):
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

def random_perform(self):
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)

ランダムに有効なアクションを選択し実行する関数

def perform(self, action=None, is_timeout=False):
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)
def is_npc(self):
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

def recover_hint(self):
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のカードをプレイした際にヒントトークンを回復する.

def play_card(self, action):
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: (アクションの種類, アクションのログ)

def discard_card(self, action):
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: (アクションの種類, アクションのログ)

def give_hint(self, action):
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: (アクションの種類, アクションのログ)

def valid_actions(self):
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): 有効なアクションのリスト

def check_game_end(self):
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

def next_turn(self):
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() # ターン開始時間を更新

次のターンに移る処理

def format_card(self, card):
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)

def print_game_state(self):
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: ゲームの状態を表す文字

def cui_game_run(self):
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環境でゲームを実行する関数.

ゲーム終了条件を満たすまでループし,各ターンの状態を表示しながらアクションを実行する.

def get_score(self):
783  def get_score(self):
784    """
785    現在のスコアを取得する関数
786
787    Returns:
788        int: ゲームのスコア
789    """
790    return sum(self.board.values())

現在のスコアを取得する関数

Returns:

int: ゲームのスコア

def game_end(self):
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      )

ゲーム終了時の処理を行う関数

def print_and_append_gui_log(self, log):
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): ログメッセージ
def get_now_unix(self):
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時間

def timelimit_action(self):
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: 実行されるアクション