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)
bp = <Blueprint 'game'>
def set_player_session(player_id, player_sid, room_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
def delete_player_session(player_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
def get_player_session(player_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(セッションなし)

def get_player_sid(player_id):
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

def get_player_room_id(player_id):
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

rooms = {}
rooms_lock = <gevent.thread.LockType object>
def set_room(room_id, room):
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): ルームオブジェクト
def delete_room(room_id):
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
def get_room(room_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

timer_queue = []
timer_lock = <gevent.thread.LockType object>
def set_timer(time, room):
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): タイマーが適用されるルームオブジェクト
def delete_timer(time, 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): 削除対象のルームオブジェクト
def process_timer(app):
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 アプリケーションインスタンス
class Room:
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): ゲームが終了したかどうかのフラグ
Room(max_player=2, npc_num=0)
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)
room_id
game
max_player
npc_num
human_player_num
players
time_limit
timeout
timeout_count
timeout_limit
game_id
game_end_flag
def add_player(self, player_id, player_sid):
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

def assign_ai(self):
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 エージェント

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

def reconnect(self, player_id, player_sid):
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
def auto_perform(self):
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 のターンに移る.
def start_game(self):
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()

ルームのゲームを開始する関数.

def turn_of_npc(self):
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 のターンであれば,自動的にアクションを実行する.
  • ゲームが終了条件を満たしていれば,ゲームを終了する.
def perform(self, player_id, action):
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): 実行するアクションのデータ
def hanabi_msg(self, player_id):
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
def hanabi_msg_all(self):
432    def hanabi_msg_all(self):
433        """
434        ルーム内のすべてのプレイヤーにゲームの状態を送信する関数.
435        """
436        for idx, player_id in enumerate(self.players):
437            self.hanabi_msg(player_id)

ルーム内のすべてのプレイヤーにゲームの状態を送信する関数.

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

現在のターンのタイマーをリセットする関数.

def check_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

def game_end(self, reason=None):
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)
def leave_player(self, player_id):
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
def is_time_in_range(start_time, end_time):
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

@bp.route('/game', methods=['GET', 'POST'])
def game():
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: ゲーム画面を表示するか,適切なページへリダイレクトする.

@bp.route('/game-connection-rejected')
def game_connection_rejected():
628@bp.route('/game-connection-rejected')
629def game_connection_rejected():
630    """
631    ゲーム接続を拒否した際のメッセージを表示する関数.
632
633    Returns:
634        str: 拒否メッセージ
635    """
636    return '新規ゲームコネクションの受付時間外です'

ゲーム接続を拒否した際のメッセージを表示する関数.

Returns:

str: 拒否メッセージ

def login_required_socket(f):
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: 認証状態をチェックするラップ関数

def handle_connect(*args, **kwargs):
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 に接続した際の処理を行う関数.

@socketio.on('action')
def handle_action(data):
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): アクションデータ
@socketio.on('game_end')
def handle_game_end(data):
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): 終了リクエストデータ