hanabiapp.views.game
1import random 2import uuid 3import json 4from datetime import datetime, timedelta 5from datetime import time as dtime 6import time 7import threading 8import heapq 9from flask import Blueprint, render_template, redirect, request, url_for, flash, current_app, session, jsonify 10from flask_login import login_required, current_user 11from flask_socketio import disconnect 12from .. import db, redis_client 13from ..models.user import User 14from ..models.game_info import GameInfo 15from ..models.participant import Participant 16from ..models.play_log import PlayLog 17from ..models.game_survey import GameSurvey 18 19from .. import socketio 20from ..game.game_manager import Game 21from ..game.agent.websocket_human_agent import WebsocketHumanAgent 22from ..game.agent import available_ai_agents 23 24bp = Blueprint('game', __name__) 25 26with open('./hanabiapp/config/ai_assignment.json', 'r', encoding='utf-8') as f: 27 ai_assignment_rule = json.load(f) 28 29assignable_agents = [ available_ai_agents[agent_name] for agent_name in ai_assignment_rule.get('random_assignable_agents', [])] 30 31def set_player_session(player_id, player_sid, room_id): 32 """ 33 プレイヤーのセッション情報を Redis に保存する関数. 34 35 Args: 36 player_id (int): プレイヤーの ID 37 player_sid (str): プレイヤーのセッション ID 38 room_id (str): プレイヤーが属するルームの ID 39 """ 40 redis_client.hset(f'player_id:{player_id}',mapping={'sid':player_sid, 'room_id':room_id}) 41 42def delete_player_session(player_id): 43 """ 44 プレイヤーのセッション情報を削除する関数. 45 46 Args: 47 player_id (int): プレイヤーの ID 48 """ 49 redis_client.delete(f'player_id:{player_id}') 50 51def get_player_session(player_id): 52 """ 53 プレイヤーのセッション情報を取得する関数. 54 55 Args: 56 player_id (int): プレイヤーの ID 57 58 Returns: 59 dict or None: セッション情報(SID, ルーム ID)または None(セッションなし) 60 """ 61 player_session = redis_client.hgetall(f'player_id:{player_id}') 62 if player_session: 63 result = { key.decode(): value.decode() for key, value in player_session.items() } 64 return result 65 else: 66 return None 67 68def get_player_sid(player_id): 69 """ 70 プレイヤーのセッション ID(SID)を取得する関数. 71 72 Args: 73 player_id (int): プレイヤーの ID 74 75 Returns: 76 str or None: セッション ID または None 77 """ 78 sid = redis_client.hget(f'player_id:{player_id}', 'sid') 79 if sid: 80 return sid.decode() 81 else: 82 return None 83 84def get_player_room_id(player_id): 85 """ 86 プレイヤーが所属するルーム ID を取得する関数. 87 88 Args: 89 player_id (int): プレイヤーの ID 90 91 Returns: 92 str or None: ルーム ID または None 93 """ 94 room_id = redis_client.hget(f'player_id:{player_id}', 'room_id') 95 if room_id: 96 return room_id.decode() 97 else: 98 return None 99 100rooms = {} 101rooms_lock = threading.Lock() 102 103def set_room(room_id, room): 104 """ 105 指定されたルーム ID にルームオブジェクトを関連付ける関数. 106 107 Args: 108 room_id (str): ルームの ID 109 room (Room): ルームオブジェクト 110 """ 111 global rooms 112 with rooms_lock: 113 rooms[room_id] = room 114 115def delete_room(room_id): 116 """ 117 指定されたルームを削除する関数. 118 119 Args: 120 room_id (str): 削除するルームの ID 121 """ 122 global rooms 123 with rooms_lock: 124 rooms.pop(room_id) 125 126def get_room(room_id): 127 """ 128 指定されたルーム ID に対応するルームオブジェクトを取得する関数. 129 130 Args: 131 room_id (str): ルームの ID 132 133 Returns: 134 Room or None: ルームオブジェクトまたは None 135 """ 136 137 global rooms 138 result = rooms.get(room_id, None) 139 return result 140 141timer_queue = [] 142timer_lock = threading.Lock() 143 144def set_timer(time, room): 145 """ 146 指定された時間にルームのタイマーを設定する関数. 147 148 Args: 149 time (datetime): タイマーの設定時刻 150 room (Room): タイマーが適用されるルームオブジェクト 151 """ 152 with timer_lock: 153 heapq.heappush(timer_queue, (time, room)) 154 155def delete_timer(time, room): 156 """ 157 指定された時間のタイマーを削除する関数. 158 159 Args: 160 time (datetime): 削除するタイマーの時刻 161 room (Room): 削除対象のルームオブジェクト 162 """ 163 with timer_lock: 164 try: 165 timer_queue.remove((time, room)) 166 heapq.heapify(timer_queue) 167 except ValueError: 168 pass 169 170def process_timer(app): 171 """ 172 ゲームのタイマーを管理し,自動実行を処理する関数. 173 174 Args: 175 app (Flask): Flask アプリケーションインスタンス 176 """ 177 178 global timer_queue 179 while True: 180 next_timeout = 0.1 181 flag = False 182 with timer_lock: 183 if timer_queue: 184 current_time = datetime.now() 185 next_time, room = timer_queue[0] 186 if next_time <= current_time: 187 flag = True 188 heapq.heappop(timer_queue) 189 else: 190 next_timeout = (next_time - current_time).total_seconds() 191 if flag: 192 with app.app_context(): 193 room.auto_perform() 194 socketio.sleep(next_timeout) 195 196class Room: 197 """ 198 ゲームルームを管理するクラス. 199 200 Attributes: 201 room_id (str): ルームの一意の ID 202 game (Game or None): ルーム内で進行中のゲーム 203 max_player (int): ルームの最大プレイヤー数 204 npc_num (int): ルーム内の NPC 数 205 players (list): ルーム内のプレイヤー ID のリスト 206 time_limit (timedelta): 1 ターンの制限時間 207 timeout (datetime): 現在のターンのタイムアウト時刻 208 timeout_count (int): タイムアウトの回数 209 timeout_limit (int): 許容される最大タイムアウト回数 210 game_id (int): ルーム内で行われるゲームの ID 211 game_end_flag (bool): ゲームが終了したかどうかのフラグ 212 """ 213 214 def __init__(self, max_player=2, npc_num=0): 215 """ 216 Room クラスのコンストラクタ. 217 218 Args: 219 max_player (int, optional): ルームの最大プレイヤー数(デフォルトは2) 220 npc_num (int, optional): ルーム内の NPC 数(デフォルトは0) 221 """ 222 223 self.room_id = str(uuid.uuid4()) 224 self.game = None 225 self.max_player = max_player 226 self.npc_num = npc_num 227 self.human_player_num = max_player - npc_num 228 self.players = [] 229 self.time_limit = timedelta(seconds=50) 230 self.timeout = datetime.now() + self.time_limit 231 set_room(self.room_id, self) 232 self.timeout_count = 0 233 self.timeout_limit = 3 234 self.game_id = 0 235 self.game_end_flag = False 236 237 def add_player(self, player_id, player_sid): 238 """ 239 ルームにプレイヤーを追加する関数. 240 241 Args: 242 player_id (int): 追加するプレイヤーの ID 243 player_sid (str): プレイヤーのセッション ID 244 245 Returns: 246 bool: プレイヤーが追加された場合は True,それ以外は False 247 """ 248 if len(self.players) < self.human_player_num: 249 self.players.append(player_id) 250 set_player_session(player_id, player_sid, self.room_id) 251 socketio.emit('room_id', f'{self.room_id}', to=player_sid) 252 if len(self.players) >= self.human_player_num: 253 self.start_game() 254 return True 255 return False 256 257 def assign_ai(self): 258 """ 259 ルーム内の NPC に適用する AI エージェントを決定する関数. 260 261 Returns: 262 Agent: 選択された AI エージェント 263 """ 264 player_id = self.players[0] 265 game_play_num = db.session.query(User.total_game_play_num).filter_by(id=player_id).scalar() 266 if player_id % 2 == 1: 267 return assignable_agents[game_play_num % 2] 268 else: 269 return assignable_agents[(game_play_num + 1) % 2] 270 271# if assignable_agents: 272# default_agent = random.choice(list(available_ai_agents.values())) 273# else: 274# default_agent = random.choice(assignable_agents) 275# if self.human_player_num > 1: 276# return default_agent 277# rules = ai_assignment_rule.get('assignment_rules') 278# player_game_count = db.session.query(Participant).filter_by(user_id=self.players[0]).count() 279# agent = None 280# if not rules: 281# return default_agent 282# for rule in rules: 283# if rule['count'] < 0: 284# agent = available_ai_agents.get(rule['agent_name']) 285# break 286# player_game_count -= rule['count'] 287# if player_game_count < 0: 288# agent = available_ai_agents.get(rule['agent_name']) 289# break 290# if agent: 291# return agent 292# else: 293# return default_agent 294 295 def is_full(self): 296 """ 297 ルームが満員かどうかを判定する関数. 298 299 Returns: 300 bool: 満員なら True,そうでなければ False 301 """ 302 return len(self.players) >= self.max_player 303 304 def reconnect(self, player_id, player_sid): 305 """ 306 プレイヤーがルームに再接続するための関数. 307 308 - プレイヤーのセッション情報を更新し,現在のゲーム状態を送信する. 309 - ゲームが終了していた場合は,クライアントに通知する. 310 311 Args: 312 player_id (int): 再接続するプレイヤーの ID 313 player_sid (str): プレイヤーの新しいセッション ID 314 """ 315 player_session = get_player_session(player_id) 316 set_player_session(player_id, player_sid, self.room_id) 317 socketio.emit('room_id', f'{self.room_id}', to=player_sid) 318 self.hanabi_msg(player_id) 319 if self.game_end_flag: 320 socketio.emit('game_end', {'game_id': self.game_id} , to=player_sid) 321 322 def auto_perform(self): 323 """ 324 タイムアウト時に自動的にアクションを実行する関数. 325 326 - タイムアウト回数が上限に達した場合,ゲームを終了する. 327 - NPC でなければ,ランダムなアクションを実行する. 328 - ゲームが終了していない場合,NPC のターンに移る. 329 """ 330 self.timeout_count += 1 331 if self.timeout_count >= self.timeout_limit: 332 self.game_end('DISCONNECTED') 333 return 334 if self.game.is_npc(): 335 return 336 self.game.random_perform() 337 self.reset_timer() 338 self.hanabi_msg_all() 339 340 if self.game.check_game_end(): 341 self.game_end() 342 else: 343 self.turn_of_npc() 344 345 def start_game(self): 346 """ 347 ルームのゲームを開始する関数. 348 """ 349 ai_agent = self.assign_ai() 350 self.players += [ ((list(available_ai_agents.values()).index(ai_agent) + 1) * -1) for i in range(1, self.npc_num+1)] 351 random.shuffle(self.players) 352 player_agent_list = [ WebsocketHumanAgent(name=f'{player_id}', player_number=idx) if player_id >= 0 else ai_agent(name=f'{player_id}', player_number=idx) for idx, player_id in enumerate(self.players) ] 353 self.game = Game(player_agent_list, int(self.time_limit.total_seconds()), seed=random.randrange(2**32), is_cui=False) 354 self.reset_timer() 355 self.hanabi_msg_all() 356 self.turn_of_npc() 357 358 def turn_of_npc(self): 359 """ 360 NPC のターンを処理する関数. 361 362 - NPC のターンであれば,自動的にアクションを実行する. 363 - ゲームが終了条件を満たしていれば,ゲームを終了する. 364 """ 365 while self.game.is_npc(): 366 socketio.sleep(1) 367 self.game.perform() 368 self.reset_timer() 369 self.hanabi_msg_all() 370 if self.game.check_game_end(): 371 self.game_end() 372 break 373 374 def perform(self, player_id, action): 375 """ 376 プレイヤーのアクションを実行する関数. 377 378 Args: 379 player_id (int): アクションを実行するプレイヤーの ID 380 action (dict): 実行するアクションのデータ 381 """ 382 self.timeout_count = 0 383 if self.game.current_player == self.players.index(player_id): 384 self.game.print_game_state() 385 self.game.perform(action) 386 self.reset_timer() 387 self.hanabi_msg_all() 388 389 if self.game.check_game_end(): 390 self.game_end() 391 else: 392 self.turn_of_npc() 393 394 def hanabi_msg(self,player_id): 395 """ 396 指定したプレイヤーにゲームの状態を送信する関数. 397 398 Args: 399 player_id (int): メッセージを送信するプレイヤーの ID 400 """ 401 if player_id < 0: 402 return 403 player_sid = get_player_sid(player_id) 404 if player_sid: 405 idx = self.players.index(player_id) 406 msg = self.game.create_observation(idx) 407 msg['gui_log_history'] = self.game.gui_log_history 408 tmp = card_knowledge = self.game.get_card_knowledge() 409 if idx != 0: 410 msg['card_knowledge'] = tmp[idx:] + tmp[:idx] 411 else: 412 msg['card_knowledge'] = tmp 413 msg['websocket_player_pid'] = self.players.index(player_id) 414 415 # 相手AIの名前を送信 416 ai_ids = [player_id for player_id in self.players if player_id < 0] 417 # 相手AIが一人の場合のみの処理 418 if ai_ids[0] == -1: # internal-state(単純AIの名前を指定) 419 msg['ai_name'] = "alpha" 420 elif ai_ids[0] == -2: # intentional(賢いAIの名前を指定) 421 msg['ai_name'] = "beta" 422 if self.game.current_player == self.players.index(player_id): 423 msg['timeout'] = self.timeout.isoformat() 424 msg['timeout_cnt'] = self.timeout_count 425 socketio.emit('hanabimsg-my-turn', json.dumps(msg), to=player_sid) 426 else: 427 msg['timeout'] = None 428 msg['timeout_cnt'] = None 429 socketio.emit('hanabimsg-not-my-turn', json.dumps(msg), to=player_sid) 430 431 def hanabi_msg_all(self): 432 """ 433 ルーム内のすべてのプレイヤーにゲームの状態を送信する関数. 434 """ 435 for idx, player_id in enumerate(self.players): 436 self.hanabi_msg(player_id) 437 438 def reset_timer(self): 439 """ 440 現在のターンのタイマーをリセットする関数. 441 """ 442 delete_timer(self.timeout, self) 443 self.timeout = datetime.now() + self.time_limit 444 set_timer(self.timeout, self) 445 446 def check_timeout(self): 447 """ 448 現在のターンがタイムアウトしたかどうかを判定する関数. 449 450 Returns: 451 bool: タイムアウトしていれば True,そうでなければ False 452 """ 453 454 return datetime.now() >= self.timeout 455 456 def game_end(self, reason=None): 457 """ 458 ゲームを終了し,結果をデータベースに保存する関数. 459 460 Args: 461 reason (str, optional): ゲーム終了の理由(デフォルトは None) 462 """ 463 # フラグがすでに立っていたらリターン 464 # 二重game_endを防止 465 if self.game_end_flag: 466 return 467 self.game_end_flag = True 468 delete_timer(self.timeout, self) 469 self.hanabi_msg_all() 470 self.game.game_end() 471 # game_infoのDB書き込み 472 game_data = self.game.data_manager.get_game_data() 473 if reason is None: 474 reason = game_data['game_end_reason'] 475 476 game_info = GameInfo( 477 start_time = game_data['game_start_time'], 478 start_time_unix = game_data['game_start_unixtime'], 479 end_time = game_data['game_end_time'], 480 end_time_unix = game_data['game_end_unixtime'], 481 play_time = game_data['one_game_time'], 482 number_of_players = len(self.players), 483 turn_order = game_data['turn_order'], 484 final_score = game_data['final_score'], 485 final_turns = game_data['final_turns'], 486 final_hint_tokens = self.game.hints, 487 final_miss_tokens = self.game.miss, 488 max_hint_tokens = self.game.max_hints, 489 max_miss_tokens = self.game.max_miss, 490 turn_time_limit = game_data['turn_time_limit'], 491 seed = self.game.seed, 492 game_end_reason = reason, 493 deck = game_data['deck'] 494 ) 495 db.session.add(game_info) 496 db.session.commit() 497 498 # 割り当てられたゲームIDを取得 499 self.game_id = game_info.id 500 501 # ゲーム終了をクライアントに通知 502 for player_id in self.players: 503 if player_id > 0: 504 socketio.emit('game_end', {'game_id': self.game_id} , to= get_player_sid(player_id)) 505 506 # プレイログを書き込み 507 logs = game_data.pop('turn_sequence', None) 508 play_log = [PlayLog( 509 game_id = self.game_id, 510 turn = log['turn'], 511 hints = log['hints'], 512 miss = log['miss'], 513 deck_size = log['deck_size'], 514 discard_b = log['discard_b'], 515 discard_g = log['discard_g'], 516 discard_r = log['discard_r'], 517 discard_w = log['discard_w'], 518 discard_y = log['discard_y'], 519 fireworks_b = log['fireworks_b'], 520 fireworks_g = log['fireworks_g'], 521 fireworks_r = log['fireworks_r'], 522 fireworks_w = log['fireworks_w'], 523 fireworks_y = log['fireworks_y'], 524 hand_pid = log['hand_pid'], 525 hint_pid = log['hint_pid'], 526 current_pid = log['current_pid'], 527 turn_perform_unixtime = log['turn_perform_unixtime'], 528 turn_perform_time = datetime.strptime(log['turn_perform_time'], '%Y-%m-%d %H:%M:%S'), 529 think_time = log['think_time'], 530 action = json.dumps(log['action']), 531 num_of_valid_actions = log['num_of_valid_actions'], 532 score_per_turn = log['score_per_turn'], 533 action_type = log['action_type'], 534 timeout = log['is_time_limit']) for log in logs] 535 db.session.add_all(play_log) 536 db.session.commit() 537 538 # participantテーブルの書き込み 539 participant = [Participant( 540 user_id = player_id, 541 game_id = self.game_id) for player_id in self.players ] 542 db.session.add_all(participant) 543 db.session.commit() 544 545 # total_play_timeとtotal_game_play_numの更新 546 total_sec = int((game_data['game_end_time'] - game_data['game_start_time']).total_seconds()) 547 if reason == 'DISCONNECTED': 548 total_sec = 0 # 切断扱い時は総プレイ時間にはカウントしない 549 db.session.execute(db.update(User).where(User.id.in_(self.players)).values(total_play_time=User.total_play_time + total_sec, total_game_play_num=User.total_game_play_num+1)) 550 db.session.commit() 551 552 def leave_player(self, player_id): 553 """ 554 ルームからプレイヤーを削除する関数. 555 556 Args: 557 player_id (int): ルームから退出するプレイヤーの ID 558 """ 559 if not self.game_end_flag: 560 return 561 562 if player_id in self.players: 563 self.players.remove(player_id) 564 delete_player_session(player_id) 565 if len(self.players) <= self.npc_num: 566 delete_room(self.room_id) 567 568def is_time_in_range(start_time, end_time): 569 """ 570 現在時刻が指定した時間範囲内かを判定する関数. 571 572 Args: 573 start_time (datetime.time): 時間範囲の開始時刻 574 end_time (datetime.time): 時間範囲の終了時刻 575 576 Returns: 577 bool: 指定された範囲内なら True,そうでなければ False 578 """ 579 now = datetime.now().time() 580 return start_time < now < end_time 581 582@bp.route('/game', methods=['GET', 'POST']) 583@login_required 584def game(): 585 """ 586 ゲームページを表示するルート関数. 587 588 - プレイヤーが既存のゲームセッションを持っている場合は,ゲーム画面を表示. 589 - 新規セッションの場合,プレイヤーの状態を確認し,条件を満たせばゲーム画面へ遷移. 590 - ゲーム時間外の場合は接続を拒否. 591 592 Returns: 593 Response: ゲーム画面を表示するか,適切なページへリダイレクトする. 594 """ 595 player_session = get_player_session(current_user.id) 596 if player_session: 597 return render_template('game.html') 598 599# 9:00-12:00, 13:30-17:00以外の新規ゲームコネクションを拒否 600 if not(is_time_in_range(dtime(0,00), dtime(23,59)) or is_time_in_range(dtime(13,30), dtime(17,00))): 601 # if not(is_time_in_range(dtime(9,00), dtime(12,00)) or is_time_in_range(dtime(13,30), dtime(17,00))): 602 return redirect(url_for('game.game_connection_rejected')) 603 604 not_answered_survey = db.session.query(Participant.game_id).join( 605 GameInfo, 606 Participant.game_id == GameInfo.id 607 ).filter( 608 Participant.user_id == current_user.id, 609 ~db.exists().where( 610 GameSurvey.game_id == Participant.game_id, 611 GameSurvey.user_id == current_user.id 612 ), 613 GameInfo.game_end_reason != 'DISCONNECTED' 614 ).order_by(Participant.game_id.asc()).first() 615 if not_answered_survey: 616 return redirect(url_for('survey.game_survey', game_id=not_answered_survey[0])) 617 618 if current_user.consent and current_user.pre_survey: 619 return render_template('game.html') 620 else: 621 if not current_user.consent: 622 flash('実験への同意が必要が必要です', 'consent') 623 if not current_user.pre_survey: 624 flash('アンケートへの回答が必要です', 'pre_survey') 625 return redirect(url_for('home.home')) 626 627@bp.route('/game-connection-rejected') 628def game_connection_rejected(): 629 """ 630 ゲーム接続を拒否した際のメッセージを表示する関数. 631 632 Returns: 633 str: 拒否メッセージ 634 """ 635 return '新規ゲームコネクションの受付時間外です' 636 637# socketioでログインを要求するデコレータ 638def login_required_socket(f): 639 """ 640 WebSocket 接続時にログインが必要であることを確認するデコレータ. 641 642 Args: 643 f (function): WebSocket ハンドラ関数 644 645 Returns: 646 function: 認証状態をチェックするラップ関数 647 """ 648 def wrapper(*args, **kwargs): 649 if not current_user.is_authenticated: 650 disconnect() 651 return False 652 else: 653 return f(*args, **kwargs) 654 return wrapper 655 656# クライアント接続時にゲーム状態を送信 657@socketio.on('connect') 658@login_required_socket 659def handle_connect(): 660 """ 661 クライアントが WebSocket に接続した際の処理を行う関数. 662 """ 663 if not(current_user.consent and current_user.pre_survey): 664 disconnect() 665 return False 666 667 player_id = current_user.id 668 player_sid = request.sid 669 670 # 再接続処理 671 room_id = get_player_room_id(player_id) 672 if room_id: 673 room = get_room(room_id) 674 if room: 675 room.reconnect(player_id, player_sid) 676 return True 677 678 # 部屋検索 679 room = None 680 for _room_id, _room in rooms.items(): 681 if not _room.is_full(): 682 room = _room 683 684 # 部屋生成 685 if room is None: 686 room = Room(2, 1) 687 room.add_player(player_id, player_sid) 688 689# アクション受信時のイベント 690@socketio.on('action') 691def handle_action(data): 692 """ 693 クライアントから受信したアクションを処理する関数. 694 695 Args: 696 data (json): アクションデータ 697 """ 698 if data.__sizeof__() > 600: 699 disconnect() 700 return 701 data = json.loads(data) 702 room_id = data['room_id'] 703 room = get_room(room_id) 704 if room is not None: 705 room.perform(current_user.id, data['action']) 706 socketio.emit('finish_process', '{}') 707 708# ゲーム終了時のボタンによるイベント 709@socketio.on('game_end') 710def handle_game_end(data): 711 """ 712 クライアントからのゲーム終了リクエストを処理する関数. 713 714 Args: 715 data (json): 終了リクエストデータ 716 """ 717 if data.__sizeof__() > 150: 718 disconnect() 719 return 720 data = json.loads(data) 721 room_id = data['room_id'] 722 room = get_room(room_id) 723 if room is not None: 724 room.leave_player(current_user.id)
32def set_player_session(player_id, player_sid, room_id): 33 """ 34 プレイヤーのセッション情報を Redis に保存する関数. 35 36 Args: 37 player_id (int): プレイヤーの ID 38 player_sid (str): プレイヤーのセッション ID 39 room_id (str): プレイヤーが属するルームの ID 40 """ 41 redis_client.hset(f'player_id:{player_id}',mapping={'sid':player_sid, 'room_id':room_id})
プレイヤーのセッション情報を Redis に保存する関数.
Arguments:
- player_id (int): プレイヤーの ID
- player_sid (str): プレイヤーのセッション ID
- room_id (str): プレイヤーが属するルームの ID
43def delete_player_session(player_id): 44 """ 45 プレイヤーのセッション情報を削除する関数. 46 47 Args: 48 player_id (int): プレイヤーの ID 49 """ 50 redis_client.delete(f'player_id:{player_id}')
プレイヤーのセッション情報を削除する関数.
Arguments:
- player_id (int): プレイヤーの ID
52def get_player_session(player_id): 53 """ 54 プレイヤーのセッション情報を取得する関数. 55 56 Args: 57 player_id (int): プレイヤーの ID 58 59 Returns: 60 dict or None: セッション情報(SID, ルーム ID)または None(セッションなし) 61 """ 62 player_session = redis_client.hgetall(f'player_id:{player_id}') 63 if player_session: 64 result = { key.decode(): value.decode() for key, value in player_session.items() } 65 return result 66 else: 67 return None
プレイヤーのセッション情報を取得する関数.
Arguments:
- player_id (int): プレイヤーの ID
Returns:
dict or None: セッション情報(SID, ルーム ID)または None(セッションなし)
69def get_player_sid(player_id): 70 """ 71 プレイヤーのセッション ID(SID)を取得する関数. 72 73 Args: 74 player_id (int): プレイヤーの ID 75 76 Returns: 77 str or None: セッション ID または None 78 """ 79 sid = redis_client.hget(f'player_id:{player_id}', 'sid') 80 if sid: 81 return sid.decode() 82 else: 83 return None
プレイヤーのセッション ID(SID)を取得する関数.
Arguments:
- player_id (int): プレイヤーの ID
Returns:
str or None: セッション ID または None
85def get_player_room_id(player_id): 86 """ 87 プレイヤーが所属するルーム ID を取得する関数. 88 89 Args: 90 player_id (int): プレイヤーの ID 91 92 Returns: 93 str or None: ルーム ID または None 94 """ 95 room_id = redis_client.hget(f'player_id:{player_id}', 'room_id') 96 if room_id: 97 return room_id.decode() 98 else: 99 return None
プレイヤーが所属するルーム ID を取得する関数.
Arguments:
- player_id (int): プレイヤーの ID
Returns:
str or None: ルーム ID または None
104def set_room(room_id, room): 105 """ 106 指定されたルーム ID にルームオブジェクトを関連付ける関数. 107 108 Args: 109 room_id (str): ルームの ID 110 room (Room): ルームオブジェクト 111 """ 112 global rooms 113 with rooms_lock: 114 rooms[room_id] = room
指定されたルーム ID にルームオブジェクトを関連付ける関数.
Arguments:
- room_id (str): ルームの ID
- room (Room): ルームオブジェクト
116def delete_room(room_id): 117 """ 118 指定されたルームを削除する関数. 119 120 Args: 121 room_id (str): 削除するルームの ID 122 """ 123 global rooms 124 with rooms_lock: 125 rooms.pop(room_id)
指定されたルームを削除する関数.
Arguments:
- room_id (str): 削除するルームの ID
127def get_room(room_id): 128 """ 129 指定されたルーム ID に対応するルームオブジェクトを取得する関数. 130 131 Args: 132 room_id (str): ルームの ID 133 134 Returns: 135 Room or None: ルームオブジェクトまたは None 136 """ 137 138 global rooms 139 result = rooms.get(room_id, None) 140 return result
指定されたルーム ID に対応するルームオブジェクトを取得する関数.
Arguments:
- room_id (str): ルームの ID
Returns:
Room or None: ルームオブジェクトまたは None
145def set_timer(time, room): 146 """ 147 指定された時間にルームのタイマーを設定する関数. 148 149 Args: 150 time (datetime): タイマーの設定時刻 151 room (Room): タイマーが適用されるルームオブジェクト 152 """ 153 with timer_lock: 154 heapq.heappush(timer_queue, (time, room))
指定された時間にルームのタイマーを設定する関数.
Arguments:
- time (datetime): タイマーの設定時刻
- room (Room): タイマーが適用されるルームオブジェクト
156def delete_timer(time, room): 157 """ 158 指定された時間のタイマーを削除する関数. 159 160 Args: 161 time (datetime): 削除するタイマーの時刻 162 room (Room): 削除対象のルームオブジェクト 163 """ 164 with timer_lock: 165 try: 166 timer_queue.remove((time, room)) 167 heapq.heapify(timer_queue) 168 except ValueError: 169 pass
指定された時間のタイマーを削除する関数.
Arguments:
- time (datetime): 削除するタイマーの時刻
- room (Room): 削除対象のルームオブジェクト
171def process_timer(app): 172 """ 173 ゲームのタイマーを管理し,自動実行を処理する関数. 174 175 Args: 176 app (Flask): Flask アプリケーションインスタンス 177 """ 178 179 global timer_queue 180 while True: 181 next_timeout = 0.1 182 flag = False 183 with timer_lock: 184 if timer_queue: 185 current_time = datetime.now() 186 next_time, room = timer_queue[0] 187 if next_time <= current_time: 188 flag = True 189 heapq.heappop(timer_queue) 190 else: 191 next_timeout = (next_time - current_time).total_seconds() 192 if flag: 193 with app.app_context(): 194 room.auto_perform() 195 socketio.sleep(next_timeout)
ゲームのタイマーを管理し,自動実行を処理する関数.
Arguments:
- app (Flask): Flask アプリケーションインスタンス
197class Room: 198 """ 199 ゲームルームを管理するクラス. 200 201 Attributes: 202 room_id (str): ルームの一意の ID 203 game (Game or None): ルーム内で進行中のゲーム 204 max_player (int): ルームの最大プレイヤー数 205 npc_num (int): ルーム内の NPC 数 206 players (list): ルーム内のプレイヤー ID のリスト 207 time_limit (timedelta): 1 ターンの制限時間 208 timeout (datetime): 現在のターンのタイムアウト時刻 209 timeout_count (int): タイムアウトの回数 210 timeout_limit (int): 許容される最大タイムアウト回数 211 game_id (int): ルーム内で行われるゲームの ID 212 game_end_flag (bool): ゲームが終了したかどうかのフラグ 213 """ 214 215 def __init__(self, max_player=2, npc_num=0): 216 """ 217 Room クラスのコンストラクタ. 218 219 Args: 220 max_player (int, optional): ルームの最大プレイヤー数(デフォルトは2) 221 npc_num (int, optional): ルーム内の NPC 数(デフォルトは0) 222 """ 223 224 self.room_id = str(uuid.uuid4()) 225 self.game = None 226 self.max_player = max_player 227 self.npc_num = npc_num 228 self.human_player_num = max_player - npc_num 229 self.players = [] 230 self.time_limit = timedelta(seconds=50) 231 self.timeout = datetime.now() + self.time_limit 232 set_room(self.room_id, self) 233 self.timeout_count = 0 234 self.timeout_limit = 3 235 self.game_id = 0 236 self.game_end_flag = False 237 238 def add_player(self, player_id, player_sid): 239 """ 240 ルームにプレイヤーを追加する関数. 241 242 Args: 243 player_id (int): 追加するプレイヤーの ID 244 player_sid (str): プレイヤーのセッション ID 245 246 Returns: 247 bool: プレイヤーが追加された場合は True,それ以外は False 248 """ 249 if len(self.players) < self.human_player_num: 250 self.players.append(player_id) 251 set_player_session(player_id, player_sid, self.room_id) 252 socketio.emit('room_id', f'{self.room_id}', to=player_sid) 253 if len(self.players) >= self.human_player_num: 254 self.start_game() 255 return True 256 return False 257 258 def assign_ai(self): 259 """ 260 ルーム内の NPC に適用する AI エージェントを決定する関数. 261 262 Returns: 263 Agent: 選択された AI エージェント 264 """ 265 player_id = self.players[0] 266 game_play_num = db.session.query(User.total_game_play_num).filter_by(id=player_id).scalar() 267 if player_id % 2 == 1: 268 return assignable_agents[game_play_num % 2] 269 else: 270 return assignable_agents[(game_play_num + 1) % 2] 271 272# if assignable_agents: 273# default_agent = random.choice(list(available_ai_agents.values())) 274# else: 275# default_agent = random.choice(assignable_agents) 276# if self.human_player_num > 1: 277# return default_agent 278# rules = ai_assignment_rule.get('assignment_rules') 279# player_game_count = db.session.query(Participant).filter_by(user_id=self.players[0]).count() 280# agent = None 281# if not rules: 282# return default_agent 283# for rule in rules: 284# if rule['count'] < 0: 285# agent = available_ai_agents.get(rule['agent_name']) 286# break 287# player_game_count -= rule['count'] 288# if player_game_count < 0: 289# agent = available_ai_agents.get(rule['agent_name']) 290# break 291# if agent: 292# return agent 293# else: 294# return default_agent 295 296 def is_full(self): 297 """ 298 ルームが満員かどうかを判定する関数. 299 300 Returns: 301 bool: 満員なら True,そうでなければ False 302 """ 303 return len(self.players) >= self.max_player 304 305 def reconnect(self, player_id, player_sid): 306 """ 307 プレイヤーがルームに再接続するための関数. 308 309 - プレイヤーのセッション情報を更新し,現在のゲーム状態を送信する. 310 - ゲームが終了していた場合は,クライアントに通知する. 311 312 Args: 313 player_id (int): 再接続するプレイヤーの ID 314 player_sid (str): プレイヤーの新しいセッション ID 315 """ 316 player_session = get_player_session(player_id) 317 set_player_session(player_id, player_sid, self.room_id) 318 socketio.emit('room_id', f'{self.room_id}', to=player_sid) 319 self.hanabi_msg(player_id) 320 if self.game_end_flag: 321 socketio.emit('game_end', {'game_id': self.game_id} , to=player_sid) 322 323 def auto_perform(self): 324 """ 325 タイムアウト時に自動的にアクションを実行する関数. 326 327 - タイムアウト回数が上限に達した場合,ゲームを終了する. 328 - NPC でなければ,ランダムなアクションを実行する. 329 - ゲームが終了していない場合,NPC のターンに移る. 330 """ 331 self.timeout_count += 1 332 if self.timeout_count >= self.timeout_limit: 333 self.game_end('DISCONNECTED') 334 return 335 if self.game.is_npc(): 336 return 337 self.game.random_perform() 338 self.reset_timer() 339 self.hanabi_msg_all() 340 341 if self.game.check_game_end(): 342 self.game_end() 343 else: 344 self.turn_of_npc() 345 346 def start_game(self): 347 """ 348 ルームのゲームを開始する関数. 349 """ 350 ai_agent = self.assign_ai() 351 self.players += [ ((list(available_ai_agents.values()).index(ai_agent) + 1) * -1) for i in range(1, self.npc_num+1)] 352 random.shuffle(self.players) 353 player_agent_list = [ WebsocketHumanAgent(name=f'{player_id}', player_number=idx) if player_id >= 0 else ai_agent(name=f'{player_id}', player_number=idx) for idx, player_id in enumerate(self.players) ] 354 self.game = Game(player_agent_list, int(self.time_limit.total_seconds()), seed=random.randrange(2**32), is_cui=False) 355 self.reset_timer() 356 self.hanabi_msg_all() 357 self.turn_of_npc() 358 359 def turn_of_npc(self): 360 """ 361 NPC のターンを処理する関数. 362 363 - NPC のターンであれば,自動的にアクションを実行する. 364 - ゲームが終了条件を満たしていれば,ゲームを終了する. 365 """ 366 while self.game.is_npc(): 367 socketio.sleep(1) 368 self.game.perform() 369 self.reset_timer() 370 self.hanabi_msg_all() 371 if self.game.check_game_end(): 372 self.game_end() 373 break 374 375 def perform(self, player_id, action): 376 """ 377 プレイヤーのアクションを実行する関数. 378 379 Args: 380 player_id (int): アクションを実行するプレイヤーの ID 381 action (dict): 実行するアクションのデータ 382 """ 383 self.timeout_count = 0 384 if self.game.current_player == self.players.index(player_id): 385 self.game.print_game_state() 386 self.game.perform(action) 387 self.reset_timer() 388 self.hanabi_msg_all() 389 390 if self.game.check_game_end(): 391 self.game_end() 392 else: 393 self.turn_of_npc() 394 395 def hanabi_msg(self,player_id): 396 """ 397 指定したプレイヤーにゲームの状態を送信する関数. 398 399 Args: 400 player_id (int): メッセージを送信するプレイヤーの ID 401 """ 402 if player_id < 0: 403 return 404 player_sid = get_player_sid(player_id) 405 if player_sid: 406 idx = self.players.index(player_id) 407 msg = self.game.create_observation(idx) 408 msg['gui_log_history'] = self.game.gui_log_history 409 tmp = card_knowledge = self.game.get_card_knowledge() 410 if idx != 0: 411 msg['card_knowledge'] = tmp[idx:] + tmp[:idx] 412 else: 413 msg['card_knowledge'] = tmp 414 msg['websocket_player_pid'] = self.players.index(player_id) 415 416 # 相手AIの名前を送信 417 ai_ids = [player_id for player_id in self.players if player_id < 0] 418 # 相手AIが一人の場合のみの処理 419 if ai_ids[0] == -1: # internal-state(単純AIの名前を指定) 420 msg['ai_name'] = "alpha" 421 elif ai_ids[0] == -2: # intentional(賢いAIの名前を指定) 422 msg['ai_name'] = "beta" 423 if self.game.current_player == self.players.index(player_id): 424 msg['timeout'] = self.timeout.isoformat() 425 msg['timeout_cnt'] = self.timeout_count 426 socketio.emit('hanabimsg-my-turn', json.dumps(msg), to=player_sid) 427 else: 428 msg['timeout'] = None 429 msg['timeout_cnt'] = None 430 socketio.emit('hanabimsg-not-my-turn', json.dumps(msg), to=player_sid) 431 432 def hanabi_msg_all(self): 433 """ 434 ルーム内のすべてのプレイヤーにゲームの状態を送信する関数. 435 """ 436 for idx, player_id in enumerate(self.players): 437 self.hanabi_msg(player_id) 438 439 def reset_timer(self): 440 """ 441 現在のターンのタイマーをリセットする関数. 442 """ 443 delete_timer(self.timeout, self) 444 self.timeout = datetime.now() + self.time_limit 445 set_timer(self.timeout, self) 446 447 def check_timeout(self): 448 """ 449 現在のターンがタイムアウトしたかどうかを判定する関数. 450 451 Returns: 452 bool: タイムアウトしていれば True,そうでなければ False 453 """ 454 455 return datetime.now() >= self.timeout 456 457 def game_end(self, reason=None): 458 """ 459 ゲームを終了し,結果をデータベースに保存する関数. 460 461 Args: 462 reason (str, optional): ゲーム終了の理由(デフォルトは None) 463 """ 464 # フラグがすでに立っていたらリターン 465 # 二重game_endを防止 466 if self.game_end_flag: 467 return 468 self.game_end_flag = True 469 delete_timer(self.timeout, self) 470 self.hanabi_msg_all() 471 self.game.game_end() 472 # game_infoのDB書き込み 473 game_data = self.game.data_manager.get_game_data() 474 if reason is None: 475 reason = game_data['game_end_reason'] 476 477 game_info = GameInfo( 478 start_time = game_data['game_start_time'], 479 start_time_unix = game_data['game_start_unixtime'], 480 end_time = game_data['game_end_time'], 481 end_time_unix = game_data['game_end_unixtime'], 482 play_time = game_data['one_game_time'], 483 number_of_players = len(self.players), 484 turn_order = game_data['turn_order'], 485 final_score = game_data['final_score'], 486 final_turns = game_data['final_turns'], 487 final_hint_tokens = self.game.hints, 488 final_miss_tokens = self.game.miss, 489 max_hint_tokens = self.game.max_hints, 490 max_miss_tokens = self.game.max_miss, 491 turn_time_limit = game_data['turn_time_limit'], 492 seed = self.game.seed, 493 game_end_reason = reason, 494 deck = game_data['deck'] 495 ) 496 db.session.add(game_info) 497 db.session.commit() 498 499 # 割り当てられたゲームIDを取得 500 self.game_id = game_info.id 501 502 # ゲーム終了をクライアントに通知 503 for player_id in self.players: 504 if player_id > 0: 505 socketio.emit('game_end', {'game_id': self.game_id} , to= get_player_sid(player_id)) 506 507 # プレイログを書き込み 508 logs = game_data.pop('turn_sequence', None) 509 play_log = [PlayLog( 510 game_id = self.game_id, 511 turn = log['turn'], 512 hints = log['hints'], 513 miss = log['miss'], 514 deck_size = log['deck_size'], 515 discard_b = log['discard_b'], 516 discard_g = log['discard_g'], 517 discard_r = log['discard_r'], 518 discard_w = log['discard_w'], 519 discard_y = log['discard_y'], 520 fireworks_b = log['fireworks_b'], 521 fireworks_g = log['fireworks_g'], 522 fireworks_r = log['fireworks_r'], 523 fireworks_w = log['fireworks_w'], 524 fireworks_y = log['fireworks_y'], 525 hand_pid = log['hand_pid'], 526 hint_pid = log['hint_pid'], 527 current_pid = log['current_pid'], 528 turn_perform_unixtime = log['turn_perform_unixtime'], 529 turn_perform_time = datetime.strptime(log['turn_perform_time'], '%Y-%m-%d %H:%M:%S'), 530 think_time = log['think_time'], 531 action = json.dumps(log['action']), 532 num_of_valid_actions = log['num_of_valid_actions'], 533 score_per_turn = log['score_per_turn'], 534 action_type = log['action_type'], 535 timeout = log['is_time_limit']) for log in logs] 536 db.session.add_all(play_log) 537 db.session.commit() 538 539 # participantテーブルの書き込み 540 participant = [Participant( 541 user_id = player_id, 542 game_id = self.game_id) for player_id in self.players ] 543 db.session.add_all(participant) 544 db.session.commit() 545 546 # total_play_timeとtotal_game_play_numの更新 547 total_sec = int((game_data['game_end_time'] - game_data['game_start_time']).total_seconds()) 548 if reason == 'DISCONNECTED': 549 total_sec = 0 # 切断扱い時は総プレイ時間にはカウントしない 550 db.session.execute(db.update(User).where(User.id.in_(self.players)).values(total_play_time=User.total_play_time + total_sec, total_game_play_num=User.total_game_play_num+1)) 551 db.session.commit() 552 553 def leave_player(self, player_id): 554 """ 555 ルームからプレイヤーを削除する関数. 556 557 Args: 558 player_id (int): ルームから退出するプレイヤーの ID 559 """ 560 if not self.game_end_flag: 561 return 562 563 if player_id in self.players: 564 self.players.remove(player_id) 565 delete_player_session(player_id) 566 if len(self.players) <= self.npc_num: 567 delete_room(self.room_id)
ゲームルームを管理するクラス.
Attributes:
- room_id (str): ルームの一意の ID
- game (Game or None): ルーム内で進行中のゲーム
- max_player (int): ルームの最大プレイヤー数
- npc_num (int): ルーム内の NPC 数
- players (list): ルーム内のプレイヤー ID のリスト
- time_limit (timedelta): 1 ターンの制限時間
- timeout (datetime): 現在のターンのタイムアウト時刻
- timeout_count (int): タイムアウトの回数
- timeout_limit (int): 許容される最大タイムアウト回数
- game_id (int): ルーム内で行われるゲームの ID
- game_end_flag (bool): ゲームが終了したかどうかのフラグ
215 def __init__(self, max_player=2, npc_num=0): 216 """ 217 Room クラスのコンストラクタ. 218 219 Args: 220 max_player (int, optional): ルームの最大プレイヤー数(デフォルトは2) 221 npc_num (int, optional): ルーム内の NPC 数(デフォルトは0) 222 """ 223 224 self.room_id = str(uuid.uuid4()) 225 self.game = None 226 self.max_player = max_player 227 self.npc_num = npc_num 228 self.human_player_num = max_player - npc_num 229 self.players = [] 230 self.time_limit = timedelta(seconds=50) 231 self.timeout = datetime.now() + self.time_limit 232 set_room(self.room_id, self) 233 self.timeout_count = 0 234 self.timeout_limit = 3 235 self.game_id = 0 236 self.game_end_flag = False
Room クラスのコンストラクタ.
Arguments:
- max_player (int, optional): ルームの最大プレイヤー数(デフォルトは2)
- npc_num (int, optional): ルーム内の NPC 数(デフォルトは0)
238 def add_player(self, player_id, player_sid): 239 """ 240 ルームにプレイヤーを追加する関数. 241 242 Args: 243 player_id (int): 追加するプレイヤーの ID 244 player_sid (str): プレイヤーのセッション ID 245 246 Returns: 247 bool: プレイヤーが追加された場合は True,それ以外は False 248 """ 249 if len(self.players) < self.human_player_num: 250 self.players.append(player_id) 251 set_player_session(player_id, player_sid, self.room_id) 252 socketio.emit('room_id', f'{self.room_id}', to=player_sid) 253 if len(self.players) >= self.human_player_num: 254 self.start_game() 255 return True 256 return False
ルームにプレイヤーを追加する関数.
Arguments:
- player_id (int): 追加するプレイヤーの ID
- player_sid (str): プレイヤーのセッション ID
Returns:
bool: プレイヤーが追加された場合は True,それ以外は False
258 def assign_ai(self): 259 """ 260 ルーム内の NPC に適用する AI エージェントを決定する関数. 261 262 Returns: 263 Agent: 選択された AI エージェント 264 """ 265 player_id = self.players[0] 266 game_play_num = db.session.query(User.total_game_play_num).filter_by(id=player_id).scalar() 267 if player_id % 2 == 1: 268 return assignable_agents[game_play_num % 2] 269 else: 270 return assignable_agents[(game_play_num + 1) % 2]
ルーム内の NPC に適用する AI エージェントを決定する関数.
Returns:
Agent: 選択された AI エージェント
296 def is_full(self): 297 """ 298 ルームが満員かどうかを判定する関数. 299 300 Returns: 301 bool: 満員なら True,そうでなければ False 302 """ 303 return len(self.players) >= self.max_player
ルームが満員かどうかを判定する関数.
Returns:
bool: 満員なら True,そうでなければ False
305 def reconnect(self, player_id, player_sid): 306 """ 307 プレイヤーがルームに再接続するための関数. 308 309 - プレイヤーのセッション情報を更新し,現在のゲーム状態を送信する. 310 - ゲームが終了していた場合は,クライアントに通知する. 311 312 Args: 313 player_id (int): 再接続するプレイヤーの ID 314 player_sid (str): プレイヤーの新しいセッション ID 315 """ 316 player_session = get_player_session(player_id) 317 set_player_session(player_id, player_sid, self.room_id) 318 socketio.emit('room_id', f'{self.room_id}', to=player_sid) 319 self.hanabi_msg(player_id) 320 if self.game_end_flag: 321 socketio.emit('game_end', {'game_id': self.game_id} , to=player_sid)
プレイヤーがルームに再接続するための関数.
- プレイヤーのセッション情報を更新し,現在のゲーム状態を送信する.
- ゲームが終了していた場合は,クライアントに通知する.
Arguments:
- player_id (int): 再接続するプレイヤーの ID
- player_sid (str): プレイヤーの新しいセッション ID
323 def auto_perform(self): 324 """ 325 タイムアウト時に自動的にアクションを実行する関数. 326 327 - タイムアウト回数が上限に達した場合,ゲームを終了する. 328 - NPC でなければ,ランダムなアクションを実行する. 329 - ゲームが終了していない場合,NPC のターンに移る. 330 """ 331 self.timeout_count += 1 332 if self.timeout_count >= self.timeout_limit: 333 self.game_end('DISCONNECTED') 334 return 335 if self.game.is_npc(): 336 return 337 self.game.random_perform() 338 self.reset_timer() 339 self.hanabi_msg_all() 340 341 if self.game.check_game_end(): 342 self.game_end() 343 else: 344 self.turn_of_npc()
タイムアウト時に自動的にアクションを実行する関数.
- タイムアウト回数が上限に達した場合,ゲームを終了する.
- NPC でなければ,ランダムなアクションを実行する.
- ゲームが終了していない場合,NPC のターンに移る.
346 def start_game(self): 347 """ 348 ルームのゲームを開始する関数. 349 """ 350 ai_agent = self.assign_ai() 351 self.players += [ ((list(available_ai_agents.values()).index(ai_agent) + 1) * -1) for i in range(1, self.npc_num+1)] 352 random.shuffle(self.players) 353 player_agent_list = [ WebsocketHumanAgent(name=f'{player_id}', player_number=idx) if player_id >= 0 else ai_agent(name=f'{player_id}', player_number=idx) for idx, player_id in enumerate(self.players) ] 354 self.game = Game(player_agent_list, int(self.time_limit.total_seconds()), seed=random.randrange(2**32), is_cui=False) 355 self.reset_timer() 356 self.hanabi_msg_all() 357 self.turn_of_npc()
ルームのゲームを開始する関数.
359 def turn_of_npc(self): 360 """ 361 NPC のターンを処理する関数. 362 363 - NPC のターンであれば,自動的にアクションを実行する. 364 - ゲームが終了条件を満たしていれば,ゲームを終了する. 365 """ 366 while self.game.is_npc(): 367 socketio.sleep(1) 368 self.game.perform() 369 self.reset_timer() 370 self.hanabi_msg_all() 371 if self.game.check_game_end(): 372 self.game_end() 373 break
NPC のターンを処理する関数.
- NPC のターンであれば,自動的にアクションを実行する.
- ゲームが終了条件を満たしていれば,ゲームを終了する.
375 def perform(self, player_id, action): 376 """ 377 プレイヤーのアクションを実行する関数. 378 379 Args: 380 player_id (int): アクションを実行するプレイヤーの ID 381 action (dict): 実行するアクションのデータ 382 """ 383 self.timeout_count = 0 384 if self.game.current_player == self.players.index(player_id): 385 self.game.print_game_state() 386 self.game.perform(action) 387 self.reset_timer() 388 self.hanabi_msg_all() 389 390 if self.game.check_game_end(): 391 self.game_end() 392 else: 393 self.turn_of_npc()
プレイヤーのアクションを実行する関数.
Arguments:
- player_id (int): アクションを実行するプレイヤーの ID
- action (dict): 実行するアクションのデータ
395 def hanabi_msg(self,player_id): 396 """ 397 指定したプレイヤーにゲームの状態を送信する関数. 398 399 Args: 400 player_id (int): メッセージを送信するプレイヤーの ID 401 """ 402 if player_id < 0: 403 return 404 player_sid = get_player_sid(player_id) 405 if player_sid: 406 idx = self.players.index(player_id) 407 msg = self.game.create_observation(idx) 408 msg['gui_log_history'] = self.game.gui_log_history 409 tmp = card_knowledge = self.game.get_card_knowledge() 410 if idx != 0: 411 msg['card_knowledge'] = tmp[idx:] + tmp[:idx] 412 else: 413 msg['card_knowledge'] = tmp 414 msg['websocket_player_pid'] = self.players.index(player_id) 415 416 # 相手AIの名前を送信 417 ai_ids = [player_id for player_id in self.players if player_id < 0] 418 # 相手AIが一人の場合のみの処理 419 if ai_ids[0] == -1: # internal-state(単純AIの名前を指定) 420 msg['ai_name'] = "alpha" 421 elif ai_ids[0] == -2: # intentional(賢いAIの名前を指定) 422 msg['ai_name'] = "beta" 423 if self.game.current_player == self.players.index(player_id): 424 msg['timeout'] = self.timeout.isoformat() 425 msg['timeout_cnt'] = self.timeout_count 426 socketio.emit('hanabimsg-my-turn', json.dumps(msg), to=player_sid) 427 else: 428 msg['timeout'] = None 429 msg['timeout_cnt'] = None 430 socketio.emit('hanabimsg-not-my-turn', json.dumps(msg), to=player_sid)
指定したプレイヤーにゲームの状態を送信する関数.
Arguments:
- player_id (int): メッセージを送信するプレイヤーの ID
432 def hanabi_msg_all(self): 433 """ 434 ルーム内のすべてのプレイヤーにゲームの状態を送信する関数. 435 """ 436 for idx, player_id in enumerate(self.players): 437 self.hanabi_msg(player_id)
ルーム内のすべてのプレイヤーにゲームの状態を送信する関数.
439 def reset_timer(self): 440 """ 441 現在のターンのタイマーをリセットする関数. 442 """ 443 delete_timer(self.timeout, self) 444 self.timeout = datetime.now() + self.time_limit 445 set_timer(self.timeout, self)
現在のターンのタイマーをリセットする関数.
447 def check_timeout(self): 448 """ 449 現在のターンがタイムアウトしたかどうかを判定する関数. 450 451 Returns: 452 bool: タイムアウトしていれば True,そうでなければ False 453 """ 454 455 return datetime.now() >= self.timeout
現在のターンがタイムアウトしたかどうかを判定する関数.
Returns:
bool: タイムアウトしていれば True,そうでなければ False
457 def game_end(self, reason=None): 458 """ 459 ゲームを終了し,結果をデータベースに保存する関数. 460 461 Args: 462 reason (str, optional): ゲーム終了の理由(デフォルトは None) 463 """ 464 # フラグがすでに立っていたらリターン 465 # 二重game_endを防止 466 if self.game_end_flag: 467 return 468 self.game_end_flag = True 469 delete_timer(self.timeout, self) 470 self.hanabi_msg_all() 471 self.game.game_end() 472 # game_infoのDB書き込み 473 game_data = self.game.data_manager.get_game_data() 474 if reason is None: 475 reason = game_data['game_end_reason'] 476 477 game_info = GameInfo( 478 start_time = game_data['game_start_time'], 479 start_time_unix = game_data['game_start_unixtime'], 480 end_time = game_data['game_end_time'], 481 end_time_unix = game_data['game_end_unixtime'], 482 play_time = game_data['one_game_time'], 483 number_of_players = len(self.players), 484 turn_order = game_data['turn_order'], 485 final_score = game_data['final_score'], 486 final_turns = game_data['final_turns'], 487 final_hint_tokens = self.game.hints, 488 final_miss_tokens = self.game.miss, 489 max_hint_tokens = self.game.max_hints, 490 max_miss_tokens = self.game.max_miss, 491 turn_time_limit = game_data['turn_time_limit'], 492 seed = self.game.seed, 493 game_end_reason = reason, 494 deck = game_data['deck'] 495 ) 496 db.session.add(game_info) 497 db.session.commit() 498 499 # 割り当てられたゲームIDを取得 500 self.game_id = game_info.id 501 502 # ゲーム終了をクライアントに通知 503 for player_id in self.players: 504 if player_id > 0: 505 socketio.emit('game_end', {'game_id': self.game_id} , to= get_player_sid(player_id)) 506 507 # プレイログを書き込み 508 logs = game_data.pop('turn_sequence', None) 509 play_log = [PlayLog( 510 game_id = self.game_id, 511 turn = log['turn'], 512 hints = log['hints'], 513 miss = log['miss'], 514 deck_size = log['deck_size'], 515 discard_b = log['discard_b'], 516 discard_g = log['discard_g'], 517 discard_r = log['discard_r'], 518 discard_w = log['discard_w'], 519 discard_y = log['discard_y'], 520 fireworks_b = log['fireworks_b'], 521 fireworks_g = log['fireworks_g'], 522 fireworks_r = log['fireworks_r'], 523 fireworks_w = log['fireworks_w'], 524 fireworks_y = log['fireworks_y'], 525 hand_pid = log['hand_pid'], 526 hint_pid = log['hint_pid'], 527 current_pid = log['current_pid'], 528 turn_perform_unixtime = log['turn_perform_unixtime'], 529 turn_perform_time = datetime.strptime(log['turn_perform_time'], '%Y-%m-%d %H:%M:%S'), 530 think_time = log['think_time'], 531 action = json.dumps(log['action']), 532 num_of_valid_actions = log['num_of_valid_actions'], 533 score_per_turn = log['score_per_turn'], 534 action_type = log['action_type'], 535 timeout = log['is_time_limit']) for log in logs] 536 db.session.add_all(play_log) 537 db.session.commit() 538 539 # participantテーブルの書き込み 540 participant = [Participant( 541 user_id = player_id, 542 game_id = self.game_id) for player_id in self.players ] 543 db.session.add_all(participant) 544 db.session.commit() 545 546 # total_play_timeとtotal_game_play_numの更新 547 total_sec = int((game_data['game_end_time'] - game_data['game_start_time']).total_seconds()) 548 if reason == 'DISCONNECTED': 549 total_sec = 0 # 切断扱い時は総プレイ時間にはカウントしない 550 db.session.execute(db.update(User).where(User.id.in_(self.players)).values(total_play_time=User.total_play_time + total_sec, total_game_play_num=User.total_game_play_num+1)) 551 db.session.commit()
ゲームを終了し,結果をデータベースに保存する関数.
Arguments:
- reason (str, optional): ゲーム終了の理由(デフォルトは None)
553 def leave_player(self, player_id): 554 """ 555 ルームからプレイヤーを削除する関数. 556 557 Args: 558 player_id (int): ルームから退出するプレイヤーの ID 559 """ 560 if not self.game_end_flag: 561 return 562 563 if player_id in self.players: 564 self.players.remove(player_id) 565 delete_player_session(player_id) 566 if len(self.players) <= self.npc_num: 567 delete_room(self.room_id)
ルームからプレイヤーを削除する関数.
Arguments:
- player_id (int): ルームから退出するプレイヤーの ID
569def is_time_in_range(start_time, end_time): 570 """ 571 現在時刻が指定した時間範囲内かを判定する関数. 572 573 Args: 574 start_time (datetime.time): 時間範囲の開始時刻 575 end_time (datetime.time): 時間範囲の終了時刻 576 577 Returns: 578 bool: 指定された範囲内なら True,そうでなければ False 579 """ 580 now = datetime.now().time() 581 return start_time < now < end_time
現在時刻が指定した時間範囲内かを判定する関数.
Arguments:
- start_time (datetime.time): 時間範囲の開始時刻
- end_time (datetime.time): 時間範囲の終了時刻
Returns:
bool: 指定された範囲内なら True,そうでなければ False
583@bp.route('/game', methods=['GET', 'POST']) 584@login_required 585def game(): 586 """ 587 ゲームページを表示するルート関数. 588 589 - プレイヤーが既存のゲームセッションを持っている場合は,ゲーム画面を表示. 590 - 新規セッションの場合,プレイヤーの状態を確認し,条件を満たせばゲーム画面へ遷移. 591 - ゲーム時間外の場合は接続を拒否. 592 593 Returns: 594 Response: ゲーム画面を表示するか,適切なページへリダイレクトする. 595 """ 596 player_session = get_player_session(current_user.id) 597 if player_session: 598 return render_template('game.html') 599 600# 9:00-12:00, 13:30-17:00以外の新規ゲームコネクションを拒否 601 if not(is_time_in_range(dtime(0,00), dtime(23,59)) or is_time_in_range(dtime(13,30), dtime(17,00))): 602 # if not(is_time_in_range(dtime(9,00), dtime(12,00)) or is_time_in_range(dtime(13,30), dtime(17,00))): 603 return redirect(url_for('game.game_connection_rejected')) 604 605 not_answered_survey = db.session.query(Participant.game_id).join( 606 GameInfo, 607 Participant.game_id == GameInfo.id 608 ).filter( 609 Participant.user_id == current_user.id, 610 ~db.exists().where( 611 GameSurvey.game_id == Participant.game_id, 612 GameSurvey.user_id == current_user.id 613 ), 614 GameInfo.game_end_reason != 'DISCONNECTED' 615 ).order_by(Participant.game_id.asc()).first() 616 if not_answered_survey: 617 return redirect(url_for('survey.game_survey', game_id=not_answered_survey[0])) 618 619 if current_user.consent and current_user.pre_survey: 620 return render_template('game.html') 621 else: 622 if not current_user.consent: 623 flash('実験への同意が必要が必要です', 'consent') 624 if not current_user.pre_survey: 625 flash('アンケートへの回答が必要です', 'pre_survey') 626 return redirect(url_for('home.home'))
ゲームページを表示するルート関数.
- プレイヤーが既存のゲームセッションを持っている場合は,ゲーム画面を表示.
- 新規セッションの場合,プレイヤーの状態を確認し,条件を満たせばゲーム画面へ遷移.
- ゲーム時間外の場合は接続を拒否.
Returns:
Response: ゲーム画面を表示するか,適切なページへリダイレクトする.
628@bp.route('/game-connection-rejected') 629def game_connection_rejected(): 630 """ 631 ゲーム接続を拒否した際のメッセージを表示する関数. 632 633 Returns: 634 str: 拒否メッセージ 635 """ 636 return '新規ゲームコネクションの受付時間外です'
ゲーム接続を拒否した際のメッセージを表示する関数.
Returns:
str: 拒否メッセージ
639def login_required_socket(f): 640 """ 641 WebSocket 接続時にログインが必要であることを確認するデコレータ. 642 643 Args: 644 f (function): WebSocket ハンドラ関数 645 646 Returns: 647 function: 認証状態をチェックするラップ関数 648 """ 649 def wrapper(*args, **kwargs): 650 if not current_user.is_authenticated: 651 disconnect() 652 return False 653 else: 654 return f(*args, **kwargs) 655 return wrapper
WebSocket 接続時にログインが必要であることを確認するデコレータ.
Arguments:
- f (function): WebSocket ハンドラ関数
Returns:
function: 認証状態をチェックするラップ関数
649 def wrapper(*args, **kwargs): 650 if not current_user.is_authenticated: 651 disconnect() 652 return False 653 else: 654 return f(*args, **kwargs)
クライアントが WebSocket に接続した際の処理を行う関数.
691@socketio.on('action') 692def handle_action(data): 693 """ 694 クライアントから受信したアクションを処理する関数. 695 696 Args: 697 data (json): アクションデータ 698 """ 699 if data.__sizeof__() > 600: 700 disconnect() 701 return 702 data = json.loads(data) 703 room_id = data['room_id'] 704 room = get_room(room_id) 705 if room is not None: 706 room.perform(current_user.id, data['action']) 707 socketio.emit('finish_process', '{}')
クライアントから受信したアクションを処理する関数.
Arguments:
- data (json): アクションデータ
710@socketio.on('game_end') 711def handle_game_end(data): 712 """ 713 クライアントからのゲーム終了リクエストを処理する関数. 714 715 Args: 716 data (json): 終了リクエストデータ 717 """ 718 if data.__sizeof__() > 150: 719 disconnect() 720 return 721 data = json.loads(data) 722 room_id = data['room_id'] 723 room = get_room(room_id) 724 if room is not None: 725 room.leave_player(current_user.id)
クライアントからのゲーム終了リクエストを処理する関数.
Arguments:
- data (json): 終了リクエストデータ