2064 lines
92 KiB
Python
2064 lines
92 KiB
Python
import asyncio
|
|
import json
|
|
import random
|
|
import uuid
|
|
from typing import Dict, List, Set, Optional
|
|
from dataclasses import dataclass, asdict
|
|
from websockets.server import serve, WebSocketServerProtocol
|
|
from aiohttp import web
|
|
import os
|
|
from database_postgres import GameDatabase
|
|
|
|
@dataclass
|
|
class Card:
|
|
suit: str # hearts, diamonds, clubs, spades
|
|
rank: str # 2-10, J, Q, K, A
|
|
id: str
|
|
|
|
def to_dict(self):
|
|
return asdict(self)
|
|
|
|
@dataclass
|
|
class Player:
|
|
id: str
|
|
nickname: str
|
|
hand: List[Card]
|
|
ready: bool
|
|
score: int = 0
|
|
is_bot: bool = False
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'id': self.id,
|
|
'nickname': self.nickname,
|
|
'hand_count': len(self.hand),
|
|
'ready': self.ready,
|
|
'score': self.score,
|
|
'is_bot': self.is_bot
|
|
}
|
|
|
|
def to_dict_with_hand(self):
|
|
return {
|
|
'id': self.id,
|
|
'nickname': self.nickname,
|
|
'hand': [card.to_dict() for card in self.hand],
|
|
'ready': self.ready,
|
|
'score': self.score
|
|
}
|
|
|
|
@dataclass
|
|
class Room:
|
|
id: str
|
|
players: Dict[str, Player]
|
|
deck: List[Card]
|
|
discard_pile: List[Card]
|
|
current_player_index: int
|
|
dealer_index: int
|
|
game_started: bool
|
|
chosen_suit: Optional[str] = None
|
|
waiting_for_eight: bool = False
|
|
card_drawn_this_turn: bool = False # Флаг что карта уже взята в этом ходу
|
|
eight_draw_used: bool = False # Флаг что взятие на восьмёрку уже использовано
|
|
countdown_active: bool = False # Активен ли обратный отсчёт
|
|
countdown_task: Optional[object] = None # Задача таймера
|
|
last_loser_id: Optional[str] = None # ID проигравшего в прошлой игре (станет дилером)
|
|
deck_size: int = 52 # Размер колоды: 52 или 36 карт
|
|
creator_id: Optional[str] = None # ID создателя комнаты
|
|
is_private: bool = False # Приватная комната (не отображается в списке)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'id': self.id,
|
|
'players': [p.to_dict() for p in self.players.values()],
|
|
'player_count': len(self.players),
|
|
'game_started': self.game_started,
|
|
'deck_size': self.deck_size,
|
|
'creator_id': self.creator_id,
|
|
'is_private': self.is_private
|
|
}
|
|
|
|
class GameServer:
|
|
def __init__(self):
|
|
self.rooms: Dict[str, Room] = {}
|
|
self.clients: Dict[WebSocketServerProtocol, str] = {} # ws -> player_id
|
|
self.player_rooms: Dict[str, str] = {} # player_id -> room_id
|
|
self.shake_cooldowns: Dict[str, float] = {} # player_id -> timestamp последней тряски
|
|
self.db = GameDatabase(
|
|
host="localhost",
|
|
port=5432,
|
|
database="czechdb",
|
|
user="czech",
|
|
password="password" # Замените на ваш пароль
|
|
)
|
|
self.cleanup_task = None
|
|
|
|
async def cleanup_old_rooms(self):
|
|
"""Очистка комнат старше 24 часов"""
|
|
deleted = await self.db.cleanup_old_rooms(hours=24)
|
|
if deleted > 0:
|
|
print(f"Cleaned up {deleted} old rooms from database")
|
|
|
|
# Получаем статистику
|
|
stats = await self.db.get_room_stats()
|
|
print(f"Room stats: {stats['total_rooms']} total, {stats['active_games']} active games, "
|
|
f"{stats['inactive_1h']} inactive >1h, {stats['inactive_24h']} inactive >24h")
|
|
|
|
def cleanup_bot_only_rooms(self):
|
|
"""Удаляет комнаты где остались только боты"""
|
|
rooms_to_delete = []
|
|
for room_id, room in self.rooms.items():
|
|
# Проверяем есть ли живые игроки
|
|
human_players = [p for p in room.players.values() if not p.is_bot]
|
|
if len(human_players) == 0 and len(room.players) > 0:
|
|
rooms_to_delete.append(room_id)
|
|
|
|
for room_id in rooms_to_delete:
|
|
del self.rooms[room_id]
|
|
print(f"Deleted bot-only room: {room_id}")
|
|
|
|
return len(rooms_to_delete)
|
|
|
|
async def start_cleanup_task(self):
|
|
"""Запуск периодической очистки каждые 6 часов"""
|
|
while True:
|
|
await asyncio.sleep(6 * 60 * 60) # 6 часов
|
|
await self.cleanup_old_rooms()
|
|
|
|
async def load_rooms_from_db(self):
|
|
"""Загрузка всех комнат из базы данных"""
|
|
room_ids = await self.db.get_all_rooms()
|
|
for room_id in room_ids:
|
|
await self.load_room(room_id)
|
|
|
|
async def load_room(self, room_id: str) -> Optional[Room]:
|
|
"""Загрузка комнаты из БД"""
|
|
# print(f"Loading room {room_id} from DB...")
|
|
room_data = await self.db.load_room(room_id)
|
|
if not room_data:
|
|
# print(f"Room {room_id} not found in DB")
|
|
return None
|
|
|
|
# print(f"Room {room_id} data loaded: game_started={room_data.get('game_started')}")
|
|
|
|
# Загружаем игроков
|
|
players_data = await self.db.load_players(room_id)
|
|
players = {}
|
|
|
|
for player_data in players_data:
|
|
cards = await self.db.load_player_cards(player_data['id'])
|
|
player = Player(
|
|
id=player_data['id'],
|
|
nickname=player_data['nickname'],
|
|
hand=[Card(**card) for card in cards],
|
|
ready=player_data['ready'],
|
|
score=player_data['score']
|
|
)
|
|
players[player_data['id']] = player
|
|
|
|
# Загружаем колоду и сброс
|
|
deck_data = await self.db.load_deck(room_id)
|
|
discard_data = await self.db.load_discard_pile(room_id)
|
|
|
|
room = Room(
|
|
id=room_data['id'],
|
|
players=players,
|
|
deck=[Card(**card) for card in deck_data],
|
|
discard_pile=[Card(**card) for card in discard_data],
|
|
current_player_index=room_data['current_player_index'],
|
|
dealer_index=room_data['dealer_index'],
|
|
game_started=room_data['game_started'],
|
|
chosen_suit=room_data['chosen_suit'],
|
|
waiting_for_eight=room_data['waiting_for_eight'],
|
|
card_drawn_this_turn=room_data['card_drawn_this_turn'],
|
|
deck_size=room_data.get('deck_size', 52),
|
|
creator_id=room_data.get('creator_id'),
|
|
is_private=room_data.get('is_private', False)
|
|
)
|
|
|
|
# Инициализируем дополнительные атрибуты
|
|
room.countdown_active = False
|
|
room.countdown_task = None
|
|
room.eight_drawn_cards = set()
|
|
|
|
self.rooms[room_id] = room
|
|
# print(f"Room {room_id} loaded successfully with {len(players)} players")
|
|
|
|
# Восстанавливаем player_rooms
|
|
for player_id in players.keys():
|
|
self.player_rooms[player_id] = room_id
|
|
|
|
return room
|
|
|
|
async def save_room_to_db(self, room_id: str):
|
|
"""Сохранение комнаты в БД"""
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
return
|
|
|
|
try:
|
|
# Сохраняем комнату
|
|
await self.db.save_room({
|
|
'id': room.id,
|
|
'game_started': room.game_started,
|
|
'current_player_index': room.current_player_index,
|
|
'dealer_index': room.dealer_index,
|
|
'chosen_suit': room.chosen_suit,
|
|
'waiting_for_eight': room.waiting_for_eight,
|
|
'eight_draw_used': room.eight_draw_used,
|
|
'card_drawn_this_turn': room.card_drawn_this_turn,
|
|
'deck_size': room.deck_size,
|
|
'creator_id': room.creator_id,
|
|
'is_private': room.is_private
|
|
})
|
|
|
|
# Сохраняем игроков
|
|
player_ids = list(room.players.keys())
|
|
for idx, (player_id, player) in enumerate(room.players.items()):
|
|
await self.db.save_player({
|
|
'id': player.id,
|
|
'nickname': player.nickname,
|
|
'ready': player.ready,
|
|
'score': player.score
|
|
}, room_id, idx)
|
|
|
|
# Сохраняем карты игрока
|
|
await self.db.save_player_cards(player_id, [card.to_dict() for card in player.hand])
|
|
|
|
# Сохраняем колоду и сброс
|
|
await self.db.save_deck(room_id, [card.to_dict() for card in room.deck])
|
|
await self.db.save_discard_pile(room_id, [card.to_dict() for card in room.discard_pile])
|
|
except Exception as e:
|
|
pass # print(f"Error saving room to DB: {e}")
|
|
|
|
def create_deck(self, deck_size: int = 52) -> List[Card]:
|
|
suits = ['hearts', 'diamonds', 'clubs', 'spades']
|
|
|
|
# Для 36 карт исключаем 2, 3, 4, 5
|
|
if deck_size == 36:
|
|
ranks = ['6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
|
|
else: # 52 карты
|
|
ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
|
|
|
|
deck = []
|
|
for suit in suits:
|
|
for rank in ranks:
|
|
deck.append(Card(suit=suit, rank=rank, id=str(uuid.uuid4())))
|
|
|
|
random.shuffle(deck)
|
|
return deck
|
|
|
|
def can_play_card(self, card: Card, top_card: Card, chosen_suit: Optional[str] = None) -> bool:
|
|
# Дама может быть положена на любую карту
|
|
if card.rank == 'Q':
|
|
return True
|
|
|
|
# Если была выбрана масть дамой, проверяем её
|
|
if chosen_suit:
|
|
return card.suit == chosen_suit or card.rank == top_card.rank
|
|
|
|
# Обычная проверка: масть или номинал
|
|
return card.suit == top_card.suit or card.rank == top_card.rank
|
|
|
|
def calculate_card_points(self, card: Card) -> int:
|
|
if card.rank in ['2', '3', '4', '5', '6', '7', '8', '9', '10']:
|
|
return int(card.rank)
|
|
elif card.rank == 'J':
|
|
return 2
|
|
elif card.rank == 'Q':
|
|
return 40 if card.suit == 'spades' else 20
|
|
elif card.rank == 'K':
|
|
return 4
|
|
elif card.rank == 'A':
|
|
return 11
|
|
return 0
|
|
|
|
def calculate_hand_points(self, hand: List[Card]) -> int:
|
|
return sum(self.calculate_card_points(card) for card in hand)
|
|
|
|
def replenish_deck(self, room: Room) -> bool:
|
|
"""Пополняет колоду из сброса если карты закончились. Возвращает True если перемешивание произошло"""
|
|
if len(room.deck) == 0 and len(room.discard_pile) > 1:
|
|
# Оставляем верхнюю карту на поле
|
|
top_card = room.discard_pile[-1]
|
|
# Берём все остальные карты из сброса
|
|
cards_to_shuffle = room.discard_pile[:-1]
|
|
# Перемешиваем
|
|
random.shuffle(cards_to_shuffle)
|
|
# Делаем новой колодой
|
|
room.deck = cards_to_shuffle
|
|
# Оставляем только верхнюю карту в сбросе
|
|
room.discard_pile = [top_card]
|
|
return True
|
|
return False
|
|
|
|
async def broadcast_to_room(self, room_id: str, message: dict, exclude_player: Optional[str] = None):
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
return
|
|
|
|
for ws, player_id in self.clients.items():
|
|
if player_id in room.players and player_id != exclude_player:
|
|
try:
|
|
await ws.send(json.dumps(message))
|
|
except:
|
|
pass
|
|
|
|
async def send_to_player(self, player_id: str, message: dict):
|
|
for ws, pid in self.clients.items():
|
|
if pid == player_id:
|
|
try:
|
|
await ws.send(json.dumps(message))
|
|
except:
|
|
pass
|
|
break
|
|
|
|
async def handle_create_room(self, ws: WebSocketServerProtocol, data: dict):
|
|
nickname = data.get('nickname')
|
|
is_private = data.get('is_private', False)
|
|
|
|
if not nickname:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Nickname required'}))
|
|
return
|
|
|
|
room_id = str(uuid.uuid4())
|
|
player_id = str(uuid.uuid4())
|
|
|
|
player = Player(
|
|
id=player_id,
|
|
nickname=nickname,
|
|
hand=[],
|
|
ready=False
|
|
)
|
|
|
|
room = Room(
|
|
id=room_id,
|
|
players={player_id: player},
|
|
deck=[],
|
|
discard_pile=[],
|
|
current_player_index=0,
|
|
dealer_index=0,
|
|
game_started=False,
|
|
creator_id=player_id,
|
|
is_private=is_private
|
|
)
|
|
|
|
self.rooms[room_id] = room
|
|
self.clients[ws] = player_id
|
|
self.player_rooms[player_id] = room_id
|
|
|
|
# Сохраняем в БД
|
|
await self.save_room_to_db(room_id)
|
|
|
|
await ws.send(json.dumps({
|
|
'type': 'room_created',
|
|
'room': room.to_dict(),
|
|
'player_id': player_id,
|
|
'room_id': room_id # Отправляем room_id для URL
|
|
}))
|
|
|
|
await self.broadcast_rooms()
|
|
|
|
async def handle_create_bot_game(self, ws: WebSocketServerProtocol, data: dict):
|
|
nickname = data.get('nickname')
|
|
bot_count = data.get('bot_count', 1)
|
|
deck_size = data.get('deck_size', 52)
|
|
|
|
if not nickname:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Nickname required'}))
|
|
return
|
|
|
|
if bot_count < 1 or bot_count > 3:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Bot count must be 1-3'}))
|
|
return
|
|
|
|
if deck_size not in [36, 52]:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Deck size must be 36 or 52'}))
|
|
return
|
|
|
|
room_id = str(uuid.uuid4())
|
|
player_id = str(uuid.uuid4())
|
|
|
|
# Создаём игрока
|
|
player = Player(
|
|
id=player_id,
|
|
nickname=nickname,
|
|
hand=[],
|
|
ready=False,
|
|
is_bot=False
|
|
)
|
|
|
|
players = {player_id: player}
|
|
|
|
# Создаём ботов
|
|
available_bot_names = [
|
|
'Дружище',
|
|
'Братан',
|
|
'Чувак',
|
|
'Кент',
|
|
'Напарник',
|
|
'Товарищ',
|
|
'Приятель',
|
|
'Бывалый',
|
|
'Земеля',
|
|
'Коллега',
|
|
'Зевака',
|
|
'Компаньон',
|
|
'Соратник',
|
|
'Местный',
|
|
'Мужик'
|
|
]
|
|
selected_bot_names = random.sample(available_bot_names, min(bot_count, len(available_bot_names)))
|
|
|
|
for i in range(bot_count):
|
|
bot_id = f'bot_{uuid.uuid4()}'
|
|
bot = Player(
|
|
id=bot_id,
|
|
nickname=selected_bot_names[i],
|
|
hand=[],
|
|
ready=True, # Боты всегда готовы
|
|
is_bot=True
|
|
)
|
|
players[bot_id] = bot
|
|
|
|
room = Room(
|
|
id=room_id,
|
|
players=players,
|
|
deck=[],
|
|
discard_pile=[],
|
|
current_player_index=0,
|
|
dealer_index=0,
|
|
game_started=False,
|
|
creator_id=player_id
|
|
)
|
|
|
|
self.rooms[room_id] = room
|
|
self.clients[ws] = player_id
|
|
self.player_rooms[player_id] = room_id
|
|
|
|
# Сохраняем в БД
|
|
await self.save_room_to_db(room_id)
|
|
|
|
await ws.send(json.dumps({
|
|
'type': 'room_created',
|
|
'room': room.to_dict(),
|
|
'player_id': player_id,
|
|
'room_id': room_id
|
|
}))
|
|
|
|
await self.broadcast_rooms()
|
|
|
|
async def handle_join_room(self, ws: WebSocketServerProtocol, data: dict):
|
|
room_id = data.get('room_id')
|
|
nickname = data.get('nickname')
|
|
|
|
if not room_id or not nickname:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Room ID and nickname required'}))
|
|
return
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
await ws.send(json.dumps({
|
|
'type': 'error',
|
|
'message': 'Комната не найдена или больше не существует',
|
|
'error_code': 'room_not_found'
|
|
}))
|
|
return
|
|
|
|
if room.game_started:
|
|
await ws.send(json.dumps({
|
|
'type': 'error',
|
|
'message': 'Игра уже началась. Присоединиться невозможно.',
|
|
'error_code': 'game_started'
|
|
}))
|
|
return
|
|
|
|
# Проверяем максимальное количество игроков
|
|
if len(room.players) >= 4:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Room is full (max 4 players)'}))
|
|
return
|
|
|
|
# Нельзя присоединяться к комнатам, где у кого-либо уже есть очки
|
|
if any(p.score > 0 for p in room.players.values()):
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'В эту комнату нельзя войти: у игроков уже есть очки'}))
|
|
return
|
|
|
|
player_id = str(uuid.uuid4())
|
|
player = Player(
|
|
id=player_id,
|
|
nickname=nickname,
|
|
hand=[],
|
|
ready=False
|
|
)
|
|
|
|
room.players[player_id] = player
|
|
self.clients[ws] = player_id
|
|
self.player_rooms[player_id] = room_id
|
|
|
|
# Сохраняем в БД
|
|
await self.save_room_to_db(room_id)
|
|
|
|
await ws.send(json.dumps({
|
|
'type': 'room_joined',
|
|
'room': room.to_dict(),
|
|
'player_id': player_id,
|
|
'room_id': room_id # Отправляем room_id для URL
|
|
}))
|
|
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'player_joined',
|
|
'player': player.to_dict()
|
|
}, exclude_player=player_id)
|
|
|
|
await self.broadcast_rooms()
|
|
|
|
async def handle_toggle_ready(self, ws: WebSocketServerProtocol, data: dict):
|
|
player_id = self.clients.get(ws)
|
|
if not player_id:
|
|
return
|
|
|
|
room_id = self.player_rooms.get(player_id)
|
|
if not room_id:
|
|
return
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room or player_id not in room.players:
|
|
return
|
|
|
|
player = room.players[player_id]
|
|
player.ready = not player.ready
|
|
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'player_ready_changed',
|
|
'player_id': player_id,
|
|
'ready': player.ready
|
|
})
|
|
|
|
# Подсчитываем готовых игроков
|
|
ready_count = sum(1 for p in room.players.values() if p.ready)
|
|
total_count = len(room.players)
|
|
|
|
# Проверяем это первая игра (у всех 0 очков) или между раундами
|
|
is_first_game = all(p.score == 0 for p in room.players.values())
|
|
|
|
# Если все готовы - начинаем сразу
|
|
if total_count >= 2 and ready_count == total_count:
|
|
# Отменяем таймер если был
|
|
if room.countdown_task:
|
|
room.countdown_task.cancel()
|
|
room.countdown_task = None
|
|
room.countdown_active = False
|
|
await self.start_game(room_id)
|
|
# Если >= 2 готовы но не все - запускаем таймер ТОЛЬКО при первой игре
|
|
elif ready_count >= 2 and not room.countdown_active and is_first_game:
|
|
room.countdown_active = True
|
|
room.countdown_task = asyncio.create_task(self.start_countdown(room_id))
|
|
# Если готовых стало меньше 2 - отменяем таймер
|
|
elif ready_count < 2 and room.countdown_active:
|
|
if room.countdown_task:
|
|
room.countdown_task.cancel()
|
|
room.countdown_task = None
|
|
room.countdown_active = False
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'countdown_cancelled'
|
|
})
|
|
|
|
async def start_countdown(self, room_id: str):
|
|
"""Обратный отсчёт 24 секунды перед началом игры"""
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
return
|
|
|
|
try:
|
|
for seconds in range(24, 0, -1):
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'countdown_tick',
|
|
'seconds': seconds
|
|
})
|
|
await asyncio.sleep(1)
|
|
|
|
# Таймер закончился - начинаем игру только с готовыми игроками
|
|
room.countdown_active = False
|
|
room.countdown_task = None
|
|
|
|
# Удаляем неготовых игроков
|
|
ready_players = {pid: p for pid, p in room.players.items() if p.ready}
|
|
if len(ready_players) >= 2:
|
|
room.players = ready_players
|
|
await self.start_game(room_id)
|
|
else:
|
|
# Недостаточно готовых игроков
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'countdown_cancelled'
|
|
})
|
|
except asyncio.CancelledError:
|
|
# Таймер был отменён
|
|
pass
|
|
|
|
async def make_bot_move(self, room_id: str, bot_id: str):
|
|
"""Бот делает ход"""
|
|
await asyncio.sleep(random.uniform(0.88, 1.82)) # Задержка для реалистичности
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room or not room.game_started:
|
|
return
|
|
|
|
# Проверяем что бот еще в комнате
|
|
if bot_id not in room.players:
|
|
return
|
|
|
|
bot = room.players[bot_id]
|
|
if not bot.is_bot:
|
|
return # Это не бот
|
|
|
|
player_ids = list(room.players.keys())
|
|
|
|
# Проверяем что индекс в пределах массива
|
|
if room.current_player_index >= len(player_ids):
|
|
return
|
|
|
|
if player_ids[room.current_player_index] != bot_id:
|
|
return # Не ход бота
|
|
top_card = room.discard_pile[-1] if room.discard_pile else None
|
|
|
|
if not top_card:
|
|
return
|
|
|
|
# Находим подходящие карты
|
|
playable_cards = []
|
|
for card in bot.hand:
|
|
# Для восьмёрки: можно играть двойку из руки ИЛИ подходящую карту из eight_drawn_cards
|
|
if room.waiting_for_eight:
|
|
if card.rank == '2':
|
|
playable_cards.append(card)
|
|
elif hasattr(room, 'eight_drawn_cards') and card.id in room.eight_drawn_cards:
|
|
# Проверяем что карта действительно подходит:
|
|
# двойка, дама, восьмёрка или та же масть что и восьмёрка на столе
|
|
if (card.rank == '2' or
|
|
card.rank == 'Q' or
|
|
card.rank == '8' or
|
|
card.suit == top_card.suit):
|
|
playable_cards.append(card)
|
|
elif self.can_play_card(card, top_card, room.chosen_suit, room.waiting_for_eight):
|
|
playable_cards.append(card)
|
|
|
|
# Если есть подходящая карта - играем с приоритетом
|
|
if playable_cards:
|
|
# Приоритет: 7, 6, 8 (только для 52 карт), A (специальные карты)
|
|
priority_ranks = ['7', '6', 'A']
|
|
if room.deck_size == 52:
|
|
priority_ranks.append('8')
|
|
priority_cards = [c for c in playable_cards if c.rank in priority_ranks]
|
|
|
|
if priority_cards:
|
|
# Есть специальные карты - выбираем случайную из них
|
|
card_to_play = random.choice(priority_cards)
|
|
elif not room.waiting_for_eight:
|
|
# Нет специальных карт и не восьмёрка - выбираем карту с максимальными очками
|
|
# Сортируем по очкам (от больших к меньшим)
|
|
playable_cards.sort(key=lambda c: self.calculate_card_points(c), reverse=True)
|
|
# Берём карты с максимальными очками (может быть несколько одинаковых)
|
|
max_points = self.calculate_card_points(playable_cards[0])
|
|
max_point_cards = [c for c in playable_cards if self.calculate_card_points(c) == max_points]
|
|
card_to_play = random.choice(max_point_cards)
|
|
else:
|
|
# Восьмёрка - выбираем случайную
|
|
card_to_play = random.choice(playable_cards)
|
|
|
|
# Если дама - выбираем масть умно
|
|
chosen_suit = None
|
|
if card_to_play.rank == 'Q':
|
|
# Приоритет: масти с сильными картами (7, 6, 8 только для 52 карт, A)
|
|
priority_ranks = ['7', '6', 'A']
|
|
if room.deck_size == 52:
|
|
priority_ranks.append('8')
|
|
priority_cards = [c for c in bot.hand if c.id != card_to_play.id and c.rank in priority_ranks]
|
|
|
|
if priority_cards:
|
|
# Есть сильные карты - выбираем случайную масть из них
|
|
suits_with_priority = list(set(c.suit for c in priority_cards))
|
|
chosen_suit = random.choice(suits_with_priority)
|
|
else:
|
|
# Нет сильных карт - выбираем любую масть из руки
|
|
suits_in_hand = list(set(c.suit for c in bot.hand if c.id != card_to_play.id))
|
|
chosen_suit = random.choice(suits_in_hand) if suits_in_hand else random.choice(['hearts', 'diamonds', 'clubs', 'spades'])
|
|
|
|
# Играем карту (используем существующую логику)
|
|
await self.handle_play_card(None, {
|
|
'card_id': card_to_play.id,
|
|
'chosen_suit': chosen_suit
|
|
}, bot_id=bot_id, room_id=room_id)
|
|
else:
|
|
# Нет подходящих карт - берём карту из колоды
|
|
if room.card_drawn_this_turn:
|
|
# Карта уже взята ранее - просто пропускаем ход
|
|
await self.handle_skip_turn(None, {}, bot_id=bot_id, room_id=room_id)
|
|
return
|
|
|
|
# Берём карту
|
|
await self.handle_draw_card(None, {}, bot_id=bot_id, room_id=room_id)
|
|
|
|
# Обновляем данные комнаты после draw
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
return
|
|
|
|
# Для обычного draw вызываем бота снова
|
|
# Для восьмёрки вызов происходит в handle_draw_card
|
|
if not room.waiting_for_eight:
|
|
await asyncio.sleep(1)
|
|
await self.make_bot_move(room_id, bot_id)
|
|
|
|
def can_play_card(self, card: Card, top_card: Card, chosen_suit: str, waiting_for_eight: bool) -> bool:
|
|
"""Проверяет можно ли сыграть карту"""
|
|
if waiting_for_eight:
|
|
return card.rank == '2'
|
|
|
|
if card.rank == 'Q':
|
|
return True
|
|
|
|
if chosen_suit:
|
|
return card.suit == chosen_suit or card.rank == top_card.rank
|
|
|
|
return card.suit == top_card.suit or card.rank == top_card.rank
|
|
|
|
async def start_game(self, room_id: str):
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
return
|
|
|
|
room.game_started = True
|
|
room.deck = self.create_deck(room.deck_size)
|
|
|
|
# Раздаём по 5 карт каждому игроку
|
|
for player in room.players.values():
|
|
player.hand = [room.deck.pop() for _ in range(5)]
|
|
|
|
# Снимаем одну карту на поле
|
|
first_card = room.deck.pop()
|
|
room.discard_pile = [first_card]
|
|
|
|
# Выбираем дилера: проигравший в прошлой игре или случайно
|
|
player_ids = list(room.players.keys())
|
|
if room.last_loser_id and room.last_loser_id in room.players:
|
|
# Дилер - проигравший в прошлой игре
|
|
room.dealer_index = player_ids.index(room.last_loser_id)
|
|
else:
|
|
# Первая игра или проигравший вышел - выбираем случайно
|
|
room.dealer_index = random.randint(0, len(player_ids) - 1)
|
|
room.current_player_index = (room.dealer_index + 1) % len(player_ids)
|
|
|
|
# Обрабатываем специальные карты при старте
|
|
next_player = room.players[player_ids[room.current_player_index]]
|
|
forced_draw_player_id = None
|
|
forced_draw_count = 0
|
|
deck_was_shuffled = False
|
|
|
|
if first_card.rank == '6':
|
|
# Следующий игрок берёт 1 карту
|
|
forced_draw_player_id = player_ids[room.current_player_index]
|
|
forced_draw_count = 1
|
|
if self.replenish_deck(room):
|
|
deck_was_shuffled = True
|
|
if room.deck:
|
|
next_player.hand.append(room.deck.pop())
|
|
# Пропускаем его ход
|
|
room.current_player_index = (room.current_player_index + 1) % len(player_ids)
|
|
|
|
elif first_card.rank == '7':
|
|
# Следующий игрок берёт 2 карты
|
|
forced_draw_player_id = player_ids[room.current_player_index]
|
|
forced_draw_count = 2
|
|
for _ in range(2):
|
|
if self.replenish_deck(room):
|
|
deck_was_shuffled = True
|
|
if room.deck:
|
|
next_player.hand.append(room.deck.pop())
|
|
# Пропускаем его ход
|
|
room.current_player_index = (room.current_player_index + 1) % len(player_ids)
|
|
|
|
elif first_card.rank == '8' and room.deck_size == 52:
|
|
# Восьмёрка работает только в режиме 52 карт
|
|
# Следующий игрок должен будет брать карты
|
|
room.waiting_for_eight = True
|
|
room.eight_draw_used = False
|
|
|
|
elif first_card.rank == 'A':
|
|
# Следующий игрок пропускает ход
|
|
room.current_player_index = (room.current_player_index + 1) % len(player_ids)
|
|
|
|
elif first_card.rank == 'Q':
|
|
# Дилер выбирает масть (автоматически выбираем случайную)
|
|
suits = ['hearts', 'diamonds', 'clubs', 'spades']
|
|
room.chosen_suit = random.choice(suits)
|
|
|
|
# Сохраняем в БД асинхронно
|
|
asyncio.create_task(self.save_room_to_db(room_id))
|
|
|
|
# Отправляем каждому игроку его карты
|
|
for player_id, player in room.players.items():
|
|
# Отправляем ID карт взятых из колоды на восьмёрку (только текущему игроку)
|
|
eight_drawn_card_ids = []
|
|
if player_id == player_ids[room.current_player_index] and hasattr(room, 'eight_drawn_cards'):
|
|
eight_drawn_card_ids = list(room.eight_drawn_cards)
|
|
|
|
message = {
|
|
'type': 'game_started',
|
|
'hand': [card.to_dict() for card in player.hand],
|
|
'top_card': first_card.to_dict(),
|
|
'current_player': player_ids[room.current_player_index],
|
|
'dealer': player_ids[room.dealer_index],
|
|
'players': [p.to_dict() for p in room.players.values()],
|
|
'deck_count': len(room.deck),
|
|
'chosen_suit': room.chosen_suit,
|
|
'waiting_for_eight': room.waiting_for_eight,
|
|
'eight_draw_used': room.eight_draw_used,
|
|
'eight_drawn_cards': eight_drawn_card_ids,
|
|
'deck_size': room.deck_size
|
|
}
|
|
|
|
# Добавляем информацию о принудительном взятии карт (6 или 7)
|
|
if forced_draw_player_id and forced_draw_count > 0:
|
|
message['forced_draw_player_id'] = forced_draw_player_id
|
|
message['forced_draw_player_nickname'] = room.players[forced_draw_player_id].nickname
|
|
message['forced_draw_count'] = forced_draw_count
|
|
|
|
await self.send_to_player(player_id, message)
|
|
|
|
# Если колода была перемешана при старте, отправляем уведомление всем
|
|
if deck_was_shuffled:
|
|
for pid in room.players.keys():
|
|
await self.send_to_player(pid, {
|
|
'type': 'deck_shuffled',
|
|
'log_message': 'Колода перемешана'
|
|
})
|
|
|
|
await self.broadcast_rooms()
|
|
|
|
# Если первый игрок - бот, запускаем его ход
|
|
first_player_id = player_ids[room.current_player_index]
|
|
if room.players[first_player_id].is_bot:
|
|
asyncio.create_task(self.make_bot_move(room_id, first_player_id))
|
|
|
|
async def handle_play_card(self, ws: WebSocketServerProtocol, data: dict, bot_id: str = None, room_id: str = None):
|
|
# Для ботов используем переданные параметры
|
|
if bot_id and room_id:
|
|
player_id = bot_id
|
|
else:
|
|
player_id = self.clients.get(ws)
|
|
if not player_id:
|
|
return
|
|
|
|
room_id = self.player_rooms.get(player_id)
|
|
if not room_id:
|
|
return
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room or not room.game_started:
|
|
return
|
|
|
|
player_ids = list(room.players.keys())
|
|
if player_ids[room.current_player_index] != player_id:
|
|
if ws: # Только для реальных игроков
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Not your turn'}))
|
|
return
|
|
|
|
card_id = data.get('card_id')
|
|
chosen_suit = data.get('chosen_suit') # Для дамы
|
|
|
|
player = room.players[player_id]
|
|
card = next((c for c in player.hand if c.id == card_id), None)
|
|
|
|
if not card:
|
|
if ws: # Только для реальных игроков
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Card not found'}))
|
|
return
|
|
|
|
if not room.discard_pile:
|
|
if ws:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'No cards on table'}))
|
|
return
|
|
|
|
top_card = room.discard_pile[-1]
|
|
|
|
# Проверка на восьмёрку
|
|
if room.waiting_for_eight:
|
|
# Можно положить:
|
|
# 1. Любую двойку из руки
|
|
# 2. Карты взятые из колоды (двойка, дама или та же масть)
|
|
is_drawn_card = hasattr(room, 'eight_drawn_cards') and card.id in room.eight_drawn_cards
|
|
|
|
if card.rank != '2' and not is_drawn_card:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Must play a 2 or draw cards'}))
|
|
return
|
|
|
|
room.waiting_for_eight = False
|
|
room.eight_draw_used = False
|
|
if hasattr(room, 'eight_drawn_cards'):
|
|
room.eight_drawn_cards.clear()
|
|
else:
|
|
# Обычная проверка
|
|
if not self.can_play_card(card, top_card, room.chosen_suit, room.waiting_for_eight):
|
|
if ws: # Только для реальных игроков
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Cannot play this card'}))
|
|
return
|
|
|
|
# Убираем карту из руки и кладём на стол
|
|
player.hand.remove(card)
|
|
room.discard_pile.append(card)
|
|
|
|
# Сбрасываем chosen_suit только если кладём НЕ даму
|
|
# Для дамы масть будет установлена ниже
|
|
if card.rank != 'Q':
|
|
room.chosen_suit = None
|
|
|
|
# Обработка специальных карт ПЕРЕД проверкой победы
|
|
next_player_index = (room.current_player_index + 1) % len(player_ids)
|
|
forced_draw_player_id = None
|
|
forced_draw_count = 0
|
|
deck_was_shuffled = False
|
|
|
|
if card.rank == '6':
|
|
# Следующий игрок берёт 1 карту и пропускает ход
|
|
next_player = room.players[player_ids[next_player_index]]
|
|
forced_draw_player_id = player_ids[next_player_index]
|
|
if self.replenish_deck(room):
|
|
deck_was_shuffled = True
|
|
if room.deck:
|
|
next_player.hand.append(room.deck.pop())
|
|
forced_draw_count = 1
|
|
next_player_index = (next_player_index + 1) % len(player_ids)
|
|
|
|
elif card.rank == '7':
|
|
# Следующий игрок берёт 2 карты и пропускает ход
|
|
next_player = room.players[player_ids[next_player_index]]
|
|
forced_draw_player_id = player_ids[next_player_index]
|
|
cards_drawn = 0
|
|
for _ in range(2):
|
|
if self.replenish_deck(room):
|
|
deck_was_shuffled = True
|
|
if room.deck:
|
|
next_player.hand.append(room.deck.pop())
|
|
cards_drawn += 1
|
|
forced_draw_count = cards_drawn
|
|
next_player_index = (next_player_index + 1) % len(player_ids)
|
|
|
|
# Проверяем победу ПЕРЕД обработкой остальных специальных карт
|
|
# Если игрок выиграл на 8, A или Q - игра заканчивается без дополнительных эффектов
|
|
if len(player.hand) == 0:
|
|
await self.handle_game_end(room_id, player_id)
|
|
return
|
|
|
|
elif card.rank == 'A':
|
|
# Следующий игрок пропускает ход
|
|
next_player_index = (next_player_index + 1) % len(player_ids)
|
|
|
|
elif card.rank == '8' and room.deck_size == 52:
|
|
# Восьмёрка работает только в режиме 52 карт
|
|
# Следующий игрок должен брать карты или положить двойку
|
|
room.waiting_for_eight = True
|
|
room.eight_draw_used = False
|
|
|
|
elif card.rank == 'Q':
|
|
# Игрок выбирает масть
|
|
if chosen_suit:
|
|
room.chosen_suit = chosen_suit
|
|
|
|
room.current_player_index = next_player_index
|
|
room.card_drawn_this_turn = False # Сбрасываем флаг для следующего игрока
|
|
|
|
# Сохраняем в БД асинхронно (не ждём)
|
|
asyncio.create_task(self.save_room_to_db(room_id))
|
|
|
|
# Отправляем обновление всем игрокам параллельно
|
|
tasks = []
|
|
for pid, p in room.players.items():
|
|
# Отправляем ID карт взятых из колоды на восьмёрку (только текущему игроку)
|
|
eight_drawn_card_ids = []
|
|
if pid == player_ids[room.current_player_index] and hasattr(room, 'eight_drawn_cards'):
|
|
eight_drawn_card_ids = list(room.eight_drawn_cards)
|
|
|
|
message = {
|
|
'type': 'card_played',
|
|
'player_id': player_id,
|
|
'player_nickname': room.players[player_id].nickname,
|
|
'card': card.to_dict(),
|
|
'top_card': room.discard_pile[-1].to_dict(),
|
|
'current_player': player_ids[room.current_player_index],
|
|
'hand': [c.to_dict() for c in p.hand],
|
|
'players': [pl.to_dict() for pl in room.players.values()],
|
|
'deck_count': len(room.deck),
|
|
'chosen_suit': room.chosen_suit,
|
|
'waiting_for_eight': room.waiting_for_eight,
|
|
'eight_draw_used': room.eight_draw_used,
|
|
'card_drawn_this_turn': False,
|
|
'eight_drawn_cards': eight_drawn_card_ids,
|
|
'deck_size': room.deck_size
|
|
}
|
|
|
|
# Добавляем информацию о принудительном взятии карт (6 или 7)
|
|
if forced_draw_player_id and forced_draw_count > 0:
|
|
message['forced_draw_player_id'] = forced_draw_player_id
|
|
message['forced_draw_player_nickname'] = room.players[forced_draw_player_id].nickname
|
|
message['forced_draw_count'] = forced_draw_count
|
|
|
|
tasks.append(self.send_to_player(pid, message))
|
|
|
|
# Отправляем все сообщения одновременно
|
|
await asyncio.gather(*tasks)
|
|
|
|
# Если колода была перемешана при игре карты, отправляем уведомление всем
|
|
if deck_was_shuffled:
|
|
for pid in room.players.keys():
|
|
await self.send_to_player(pid, {
|
|
'type': 'deck_shuffled',
|
|
'log_message': 'Колода перемешана'
|
|
})
|
|
|
|
# Если следующий игрок - бот, делаем его ход
|
|
next_player_id = player_ids[room.current_player_index]
|
|
if room.players[next_player_id].is_bot:
|
|
asyncio.create_task(self.make_bot_move(room_id, next_player_id))
|
|
|
|
async def handle_draw_card(self, ws: WebSocketServerProtocol, data: dict, bot_id: str = None, room_id: str = None):
|
|
# Для ботов используем переданные параметры
|
|
if bot_id and room_id:
|
|
player_id = bot_id
|
|
else:
|
|
player_id = self.clients.get(ws)
|
|
if not player_id:
|
|
return
|
|
|
|
room_id = self.player_rooms.get(player_id)
|
|
if not room_id:
|
|
return
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room or not room.game_started:
|
|
return
|
|
|
|
player_ids = list(room.players.keys())
|
|
if player_ids[room.current_player_index] != player_id:
|
|
if ws: # Только для реальных игроков
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Not your turn'}))
|
|
return
|
|
|
|
player = room.players[player_id]
|
|
|
|
# Обработка восьмёрки
|
|
if room.waiting_for_eight:
|
|
# Проверяем использовал ли игрок уже взятие на восьмёрку
|
|
if room.eight_draw_used:
|
|
if ws:
|
|
await ws.send(json.dumps({
|
|
'type': 'error',
|
|
'message': 'Вы уже использовали взятие карт на восьмёрку'
|
|
}))
|
|
return
|
|
|
|
# Помечаем что взятие использовано
|
|
room.eight_draw_used = True
|
|
|
|
# Игрок тянет карты из колоды пока не попадётся подходящая
|
|
drawn_cards = []
|
|
drawn_card_ids = [] # ID карт взятых из колоды
|
|
deck_was_shuffled = False
|
|
max_iterations = 1000 # Защита от бесконечного цикла
|
|
iterations = 0
|
|
|
|
while iterations < max_iterations:
|
|
iterations += 1
|
|
|
|
# Пытаемся пополнить колоду из сброса если она пустая
|
|
shuffled = self.replenish_deck(room)
|
|
if shuffled:
|
|
deck_was_shuffled = True
|
|
|
|
# Если колода пустая и перемешивание не помогло
|
|
# (в игре осталась только 1 карта на столе и больше нет карт нигде)
|
|
if not room.deck:
|
|
# Это критическая ситуация - карты закончились совсем
|
|
# Игрок не может взять карту, пропускаем ход
|
|
break
|
|
|
|
# Проверяем что есть верхняя карта на столе
|
|
if not room.discard_pile:
|
|
break
|
|
|
|
card = room.deck.pop()
|
|
player.hand.append(card)
|
|
drawn_cards.append(card)
|
|
drawn_card_ids.append(card.id)
|
|
|
|
top_card = room.discard_pile[-1]
|
|
# Из колоды подходят:
|
|
# 1. Любая двойка (2)
|
|
# 2. Любая дама (Q)
|
|
# 3. Любая восьмёрка (8)
|
|
# 4. Карта той же масти что и восьмёрка
|
|
if (card.rank == '2' or
|
|
card.rank == 'Q' or
|
|
card.rank == '8' or
|
|
card.suit == top_card.suit):
|
|
# Нашли подходящую карту - выходим
|
|
break
|
|
|
|
# Если не подошла - продолжаем цикл
|
|
# При необходимости колода перемешается на следующей итерации
|
|
|
|
# НЕ сбрасываем waiting_for_eight - он сбросится когда игрок сыграет карту
|
|
# НЕ устанавливаем card_drawn_this_turn - это автоматическое взятие
|
|
|
|
# Сохраняем ID карт взятых из колоды
|
|
if not hasattr(room, 'eight_drawn_cards'):
|
|
room.eight_drawn_cards = set()
|
|
room.eight_drawn_cards.update(drawn_card_ids)
|
|
|
|
# Если не нашли подходящую карту, пропускаем ход
|
|
if not drawn_cards:
|
|
room.waiting_for_eight = False
|
|
room.eight_draw_used = False
|
|
room.eight_drawn_cards.clear()
|
|
room.current_player_index = (room.current_player_index + 1) % len(player_ids)
|
|
else:
|
|
last_card = drawn_cards[-1]
|
|
# Проверяем последнюю взятую карту
|
|
if room.discard_pile:
|
|
top_card = room.discard_pile[-1]
|
|
card_matches = (last_card.rank == '2' or
|
|
last_card.rank == 'Q' or
|
|
last_card.rank == '8' or
|
|
last_card.suit == top_card.suit)
|
|
else:
|
|
card_matches = False
|
|
|
|
if not card_matches:
|
|
room.waiting_for_eight = False
|
|
room.eight_draw_used = False
|
|
room.eight_drawn_cards.clear()
|
|
room.current_player_index = (room.current_player_index + 1) % len(player_ids)
|
|
|
|
# Если колода была перемешана во время взятия карт на восьмёрку
|
|
if deck_was_shuffled:
|
|
for pid in room.players.keys():
|
|
await self.send_to_player(pid, {
|
|
'type': 'deck_shuffled',
|
|
'log_message': 'Колода перемешана'
|
|
})
|
|
else:
|
|
# Обычное взятие карты (можно взять даже если есть чем ходить)
|
|
if room.card_drawn_this_turn:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Вы уже взяли карту в этом ходу'}))
|
|
return
|
|
|
|
deck_shuffled = self.replenish_deck(room)
|
|
|
|
if not room.deck:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Колода пуста'}))
|
|
return
|
|
|
|
card = room.deck.pop()
|
|
player.hand.append(card)
|
|
drawn_cards = [card] # Инициализируем для использования в логировании
|
|
room.card_drawn_this_turn = True
|
|
|
|
# Если колода была перемешана, отправляем лог всем
|
|
if deck_shuffled:
|
|
for pid in room.players.keys():
|
|
await self.send_to_player(pid, {
|
|
'type': 'deck_shuffled',
|
|
'log_message': 'Колода перемешана'
|
|
})
|
|
|
|
# Сохраняем в БД асинхронно (не ждём)
|
|
asyncio.create_task(self.save_room_to_db(room_id))
|
|
|
|
# Отправляем обновление (ход НЕ переходит автоматически)
|
|
for pid, p in room.players.items():
|
|
# Отправляем ID карт взятых из колоды на восьмёрку (только текущему игроку)
|
|
eight_drawn_card_ids = []
|
|
if pid == player_id and hasattr(room, 'eight_drawn_cards'):
|
|
eight_drawn_card_ids = list(room.eight_drawn_cards)
|
|
|
|
await self.send_to_player(pid, {
|
|
'type': 'card_drawn',
|
|
'player_id': player_id,
|
|
'player_nickname': room.players[player_id].nickname,
|
|
'cards_count': len(drawn_cards),
|
|
'current_player': player_ids[room.current_player_index],
|
|
'hand': [c.to_dict() for c in p.hand],
|
|
'players': [pl.to_dict() for pl in room.players.values()],
|
|
'deck_count': len(room.deck),
|
|
'waiting_for_eight': room.waiting_for_eight,
|
|
'eight_draw_used': room.eight_draw_used,
|
|
'card_drawn_this_turn': room.card_drawn_this_turn,
|
|
'chosen_suit': room.chosen_suit,
|
|
'top_card': room.discard_pile[-1].to_dict() if room.discard_pile else None,
|
|
'eight_drawn_cards': eight_drawn_card_ids
|
|
})
|
|
|
|
# Если это был бот и он взял карты на восьмёрку, вызываем его ход снова
|
|
# Для обычного draw вызов происходит в make_bot_move
|
|
if bot_id and room.waiting_for_eight:
|
|
# Бот взял карты на восьмёрку и должен сыграть одну из них
|
|
async def delayed_bot_move():
|
|
await asyncio.sleep(1)
|
|
await self.make_bot_move(room_id, bot_id)
|
|
asyncio.create_task(delayed_bot_move())
|
|
|
|
async def handle_game_end(self, room_id: str, winner_id: str):
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
return
|
|
|
|
# Проверяем последнюю карту победителя (дама даёт бонус)
|
|
winning_card = room.discard_pile[-1] if room.discard_pile else None
|
|
queen_bonus = 0
|
|
if winning_card and winning_card.rank == 'Q':
|
|
if winning_card.suit == 'spades':
|
|
queen_bonus = -40 # Пиковая дама
|
|
else:
|
|
queen_bonus = -20 # Любая другая дама
|
|
|
|
# Подсчитываем очки для всех игроков кроме победителя
|
|
results = []
|
|
for player_id, player in room.players.items():
|
|
if player_id != winner_id:
|
|
points = self.calculate_hand_points(player.hand)
|
|
player.score += points
|
|
results.append({
|
|
'player_id': player_id,
|
|
'nickname': player.nickname,
|
|
'points': points,
|
|
'total_score': player.score,
|
|
'hand': [c.to_dict() for c in player.hand]
|
|
})
|
|
else:
|
|
# Победитель получает бонус за даму
|
|
player.score += queen_bonus
|
|
results.append({
|
|
'player_id': player_id,
|
|
'nickname': player.nickname,
|
|
'points': queen_bonus,
|
|
'total_score': player.score,
|
|
'hand': [], # Победитель без карт
|
|
'queen_bonus': queen_bonus # Для отображения на клиенте
|
|
})
|
|
|
|
# Находим проигравшего в этой раздаче (игрок с максимальными очками в раздаче)
|
|
# Это тот, кто набрал больше всего очков в этой конкретной игре, не победитель
|
|
round_loser_id = None
|
|
max_round_points = -1
|
|
for result in results:
|
|
if result['player_id'] != winner_id and result['points'] > max_round_points:
|
|
max_round_points = result['points']
|
|
round_loser_id = result['player_id']
|
|
|
|
# Обрабатываем специальные случаи с очками
|
|
players_to_kick = [] # Игроки с >101 очком
|
|
players_reset = [] # Игроки с ровно 101 (обнуляются)
|
|
for player_id, player in room.players.items():
|
|
if player.score == 101:
|
|
# Ровно 101 - обнуляем очки
|
|
players_reset.append(player_id)
|
|
player.score = 0
|
|
elif player.score > 101:
|
|
# Больше 101 - игрок вылетает
|
|
players_to_kick.append(player_id)
|
|
|
|
# Добавляем флаг reset_to_zero в результаты для обнулившихся игроков
|
|
for result in results:
|
|
if result['player_id'] in players_reset:
|
|
result['reset_to_zero'] = True
|
|
|
|
# Сохраняем ID проигравшего в раздаче для следующей игры (если он не вылетает)
|
|
if round_loser_id and round_loser_id not in players_to_kick:
|
|
room.last_loser_id = round_loser_id
|
|
else:
|
|
# Если проигравший вылетает, выбираем случайно в следующий раз
|
|
room.last_loser_id = None
|
|
|
|
# Последняя карта победителя (верхняя в discard_pile)
|
|
winning_card = room.discard_pile[-1].to_dict() if room.discard_pile else None
|
|
|
|
# Отправляем результаты
|
|
for player_id in room.players.keys():
|
|
await self.send_to_player(player_id, {
|
|
'type': 'game_ended',
|
|
'winner_id': winner_id,
|
|
'winning_card': winning_card,
|
|
'results': results
|
|
})
|
|
|
|
# Удаляем игроков с >101 очком
|
|
if players_to_kick:
|
|
# Проверяем: если осталось меньше 2 игроков - закрываем комнату
|
|
remaining_players = len(room.players) - len(players_to_kick)
|
|
|
|
if remaining_players < 2:
|
|
# Закрываем комнату полностью
|
|
# Определяем победителя (игрок с минимальными очками среди оставшихся)
|
|
final_winner_id = None
|
|
min_score = float('inf')
|
|
for player_id, player in room.players.items():
|
|
if player_id not in players_to_kick and player.score < min_score:
|
|
min_score = player.score
|
|
final_winner_id = player_id
|
|
|
|
# Отправляем финальное сообщение о победе
|
|
for player_id in room.players.keys():
|
|
await self.send_to_player(player_id, {
|
|
'type': 'final_winner',
|
|
'winner_id': final_winner_id,
|
|
'winner_nickname': room.players[final_winner_id].nickname if final_winner_id else '',
|
|
'kicked_players': [room.players[pid].nickname for pid in players_to_kick]
|
|
})
|
|
|
|
# Удаляем комнату из памяти и БД
|
|
await self.db.delete_room(room_id)
|
|
if room_id in self.rooms:
|
|
del self.rooms[room_id]
|
|
|
|
# Очищаем player_rooms для всех игроков
|
|
for player_id in list(room.players.keys()):
|
|
if player_id in self.player_rooms:
|
|
del self.player_rooms[player_id]
|
|
|
|
await self.broadcast_rooms()
|
|
return
|
|
else:
|
|
# Удаляем проигравших игроков из комнаты
|
|
for player_id in players_to_kick:
|
|
player_nickname = room.players[player_id].nickname
|
|
del room.players[player_id]
|
|
if player_id in self.player_rooms:
|
|
del self.player_rooms[player_id]
|
|
|
|
# Уведомляем всех о вылете игрока
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'player_kicked',
|
|
'player_id': player_id,
|
|
'player_nickname': player_nickname,
|
|
'reason': 'Набрал больше 101 очка'
|
|
})
|
|
|
|
# Сбрасываем комнату для новой игры
|
|
room.game_started = False
|
|
for player in room.players.values():
|
|
player.hand = []
|
|
# Боты всегда готовы, реальные игроки - нет
|
|
player.ready = player.is_bot
|
|
room.deck = []
|
|
room.discard_pile = []
|
|
room.chosen_suit = None
|
|
room.waiting_for_eight = False
|
|
room.eight_draw_used = False
|
|
|
|
# Сохраняем обновлённые очки в БД асинхронно
|
|
asyncio.create_task(self.save_room_to_db(room_id))
|
|
|
|
# Отправляем обновление комнаты всем игрокам
|
|
for player_id in room.players.keys():
|
|
await self.send_to_player(player_id, {
|
|
'type': 'room_updated',
|
|
'room': room.to_dict()
|
|
})
|
|
|
|
await self.broadcast_rooms()
|
|
|
|
async def handle_skip_turn(self, ws: WebSocketServerProtocol, data: dict, bot_id: str = None, room_id: str = None):
|
|
# Для ботов используем переданные параметры
|
|
if bot_id and room_id:
|
|
player_id = bot_id
|
|
else:
|
|
player_id = self.clients.get(ws)
|
|
if not player_id:
|
|
return
|
|
|
|
room_id = self.player_rooms.get(player_id)
|
|
if not room_id:
|
|
return
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room or not room.game_started:
|
|
return
|
|
|
|
player_ids = list(room.players.keys())
|
|
if player_ids[room.current_player_index] != player_id:
|
|
if ws: # Только для реальных игроков
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Not your turn'}))
|
|
return
|
|
|
|
# Нельзя пропустить ход при восьмёрке
|
|
if room.waiting_for_eight:
|
|
if ws: # Только для реальных игроков
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Нельзя пропустить ход на восьмёрке'}))
|
|
return
|
|
|
|
# Можно пропустить ход только если карта уже была взята
|
|
if not room.card_drawn_this_turn:
|
|
if ws: # Только для реальных игроков
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Сначала возьмите карту из колоды'}))
|
|
return
|
|
|
|
# Пропускаем ход
|
|
room.current_player_index = (room.current_player_index + 1) % len(player_ids)
|
|
room.card_drawn_this_turn = False
|
|
|
|
# Сохраняем в БД асинхронно (не ждём)
|
|
asyncio.create_task(self.save_room_to_db(room_id))
|
|
|
|
# Отправляем обновление с логом
|
|
for pid, p in room.players.items():
|
|
# Отправляем ID карт взятых из колоды на восьмёрку (только текущему игроку)
|
|
eight_drawn_card_ids = []
|
|
if pid == player_ids[room.current_player_index] and hasattr(room, 'eight_drawn_cards'):
|
|
eight_drawn_card_ids = list(room.eight_drawn_cards)
|
|
|
|
await self.send_to_player(pid, {
|
|
'type': 'turn_skipped',
|
|
'player_id': player_id,
|
|
'player_nickname': room.players[player_id].nickname,
|
|
'current_player': player_ids[room.current_player_index],
|
|
'hand': [c.to_dict() for c in p.hand],
|
|
'players': [pl.to_dict() for pl in room.players.values()],
|
|
'card_drawn_this_turn': False,
|
|
'chosen_suit': room.chosen_suit,
|
|
'waiting_for_eight': room.waiting_for_eight,
|
|
'eight_draw_used': room.eight_draw_used,
|
|
'eight_drawn_cards': eight_drawn_card_ids,
|
|
'top_card': room.discard_pile[-1].to_dict() if room.discard_pile else None,
|
|
'log_message': f"{room.players[player_id].nickname} пропустил ход"
|
|
})
|
|
|
|
# Если следующий игрок - бот, делаем его ход
|
|
next_player_id = player_ids[room.current_player_index]
|
|
if room.players[next_player_id].is_bot:
|
|
asyncio.create_task(self.make_bot_move(room_id, next_player_id))
|
|
|
|
async def handle_reconnect(self, ws: WebSocketServerProtocol, data: dict):
|
|
"""Переподключение игрока по player_id"""
|
|
player_id = data.get('player_id')
|
|
room_id = data.get('room_id')
|
|
|
|
# print(f"Reconnect attempt: player_id={player_id}, room_id={room_id}")
|
|
|
|
if not player_id or not room_id:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Player ID and Room ID required'}))
|
|
return
|
|
|
|
# Проверяем существование комнаты
|
|
if room_id not in self.rooms:
|
|
# print(f"Room {room_id} not in memory, loading from DB...")
|
|
# Пытаемся загрузить из БД
|
|
await self.load_room(room_id)
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
# print(f"Room {room_id} not found in DB")
|
|
await ws.send(json.dumps({
|
|
'type': 'error',
|
|
'message': 'Комната не найдена или больше не существует',
|
|
'error_code': 'room_not_found'
|
|
}))
|
|
return
|
|
|
|
# print(f"Room {room_id} loaded, players: {list(room.players.keys())}")
|
|
|
|
# Проверяем существование игрока
|
|
if player_id not in room.players:
|
|
# print(f"Player {player_id} not found in room {room_id}")
|
|
await ws.send(json.dumps({
|
|
'type': 'error',
|
|
'message': 'Игрок не найден в этой комнате',
|
|
'error_code': 'player_not_found'
|
|
}))
|
|
return
|
|
|
|
# Переподключаем игрока
|
|
self.clients[ws] = player_id
|
|
self.player_rooms[player_id] = room_id
|
|
|
|
player = room.players[player_id]
|
|
|
|
# Уведомляем других игроков о переподключении
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'player_reconnected',
|
|
'player_id': player_id,
|
|
'nickname': player.nickname
|
|
}, exclude_player=player_id)
|
|
|
|
if room.game_started:
|
|
# Отправляем состояние игры
|
|
player_ids = list(room.players.keys())
|
|
|
|
# Отправляем ID карт взятых из колоды на восьмёрку (только текущему игроку)
|
|
eight_drawn_card_ids = []
|
|
if player_id == player_ids[room.current_player_index] and hasattr(room, 'eight_drawn_cards'):
|
|
eight_drawn_card_ids = list(room.eight_drawn_cards)
|
|
|
|
message = {
|
|
'type': 'game_started',
|
|
'hand': [card.to_dict() for card in player.hand],
|
|
'top_card': room.discard_pile[-1].to_dict() if room.discard_pile else None,
|
|
'current_player': player_ids[room.current_player_index],
|
|
'dealer': player_ids[room.dealer_index],
|
|
'players': [p.to_dict() for p in room.players.values()],
|
|
'deck_count': len(room.deck),
|
|
'chosen_suit': room.chosen_suit,
|
|
'waiting_for_eight': room.waiting_for_eight,
|
|
'eight_draw_used': room.eight_draw_used,
|
|
'eight_drawn_cards': eight_drawn_card_ids,
|
|
'card_drawn_this_turn': room.card_drawn_this_turn,
|
|
'deck_size': room.deck_size
|
|
}
|
|
|
|
await ws.send(json.dumps(message))
|
|
|
|
# Если сейчас ход бота - запускаем его
|
|
current_player_id = player_ids[room.current_player_index]
|
|
if room.players[current_player_id].is_bot:
|
|
asyncio.create_task(self.make_bot_move(room_id, current_player_id))
|
|
else:
|
|
# Отправляем состояние комнаты
|
|
await ws.send(json.dumps({
|
|
'type': 'room_joined',
|
|
'room': room.to_dict(),
|
|
'player_id': player_id,
|
|
'room_id': room_id
|
|
}))
|
|
|
|
async def broadcast_rooms(self):
|
|
# Очищаем комнаты где только боты
|
|
self.cleanup_bot_only_rooms()
|
|
|
|
available_rooms = []
|
|
for room in self.rooms.values():
|
|
# Проверяем что игра не началась и все очки 0
|
|
if not room.game_started and all(p.score == 0 for p in room.players.values()):
|
|
# Проверяем что есть хотя бы один живой игрок (не бот)
|
|
has_human = any(not p.is_bot for p in room.players.values())
|
|
# Не показываем приватные комнаты в общем списке
|
|
if has_human and not room.is_private:
|
|
available_rooms.append(room.to_dict())
|
|
|
|
message = json.dumps({
|
|
'type': 'rooms_list',
|
|
'rooms': available_rooms
|
|
})
|
|
|
|
for ws in self.clients.keys():
|
|
try:
|
|
await ws.send(message)
|
|
except:
|
|
pass
|
|
|
|
async def handle_chat_message(self, ws: WebSocketServerProtocol, data: dict):
|
|
"""Обработка сообщения чата"""
|
|
player_id = self.clients.get(ws)
|
|
# print(f"Chat message from player_id: {player_id}")
|
|
if not player_id:
|
|
# print("No player_id found")
|
|
return
|
|
|
|
room_id = self.player_rooms.get(player_id)
|
|
# print(f"Player in room: {room_id}")
|
|
if not room_id:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'You are not in a room'}))
|
|
return
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
# print(f"Room {room_id} not found")
|
|
return
|
|
|
|
player = room.players.get(player_id)
|
|
if not player:
|
|
# print(f"Player {player_id} not in room players")
|
|
return
|
|
|
|
message_text = data.get('message', '').strip()
|
|
# print(f"Message text: {message_text}")
|
|
if not message_text:
|
|
return
|
|
|
|
# Ограничиваем длину сообщения
|
|
message_text = message_text[:200]
|
|
|
|
# Отправляем сообщение всем игрокам в комнате
|
|
chat_message = f"{player.nickname}: {message_text}"
|
|
# print(f"Sending chat message to {len(room.players)} players: {chat_message}")
|
|
|
|
for pid in room.players.keys():
|
|
await self.send_to_player(pid, {
|
|
'type': 'chat_message',
|
|
'message': chat_message,
|
|
'player_id': player_id,
|
|
'player_nickname': player.nickname
|
|
})
|
|
|
|
async def handle_change_deck_size(self, ws: WebSocketServerProtocol, data: dict):
|
|
"""Изменение размера колоды (только создатель комнаты, до начала игры)"""
|
|
player_id = self.clients.get(ws)
|
|
if not player_id:
|
|
return
|
|
|
|
room_id = self.player_rooms.get(player_id)
|
|
if not room_id:
|
|
return
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
return
|
|
|
|
# Проверяем что игрок - создатель комнаты
|
|
if room.creator_id != player_id:
|
|
await ws.send(json.dumps({
|
|
'type': 'error',
|
|
'message': 'Only room creator can change deck size'
|
|
}))
|
|
return
|
|
|
|
# Проверяем что игра не началась
|
|
if room.game_started:
|
|
await ws.send(json.dumps({
|
|
'type': 'error',
|
|
'message': 'Cannot change deck size after game started'
|
|
}))
|
|
return
|
|
|
|
deck_size = data.get('deck_size')
|
|
if deck_size not in [36, 52]:
|
|
await ws.send(json.dumps({
|
|
'type': 'error',
|
|
'message': 'Deck size must be 36 or 52'
|
|
}))
|
|
return
|
|
|
|
room.deck_size = deck_size
|
|
|
|
# Сохраняем в БД
|
|
await self.save_room_to_db(room_id)
|
|
|
|
# Уведомляем всех в комнате
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'deck_size_changed',
|
|
'deck_size': deck_size
|
|
})
|
|
|
|
async def handle_toggle_private(self, ws: WebSocketServerProtocol, data: dict):
|
|
"""Переключение приватности комнаты (только создатель, до начала игры)"""
|
|
player_id = self.clients.get(ws)
|
|
if not player_id:
|
|
return
|
|
|
|
room_id = self.player_rooms.get(player_id)
|
|
if not room_id:
|
|
return
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room:
|
|
return
|
|
|
|
# Только создатель может менять приватность
|
|
if room.creator_id != player_id:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Только создатель может изменить приватность комнаты'}))
|
|
return
|
|
|
|
# Только до начала игры
|
|
if room.game_started or any(p.score > 0 for p in room.players.values()):
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Нельзя изменить приватность после начала игры'}))
|
|
return
|
|
|
|
is_private = data.get('is_private', False)
|
|
room.is_private = is_private
|
|
|
|
# Сохраняем в БД
|
|
await self.save_room_to_db(room_id)
|
|
|
|
# Уведомляем всех в комнате
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'room_privacy_changed',
|
|
'is_private': is_private
|
|
})
|
|
|
|
# Обновляем список комнат (приватные не показываются)
|
|
await self.broadcast_rooms()
|
|
|
|
async def handle_shake_discard(self, ws: WebSocketServerProtocol, data: dict):
|
|
"""Обработка тряски карты сброса - рассылаем всем игрокам в комнате"""
|
|
player_id = self.clients.get(ws)
|
|
if not player_id:
|
|
return
|
|
|
|
# Проверяем кулдаун (5 секунд)
|
|
import time
|
|
now = time.time()
|
|
cooldown = 5.0 # 5 секунд
|
|
|
|
last_shake = self.shake_cooldowns.get(player_id, 0)
|
|
if now - last_shake < cooldown:
|
|
# Просто игнорируем запрос если кулдаун активен
|
|
return
|
|
|
|
# Обновляем время последней тряски
|
|
self.shake_cooldowns[player_id] = now
|
|
|
|
# Находим комнату игрока
|
|
room_id = None
|
|
for rid, room in self.rooms.items():
|
|
if player_id in room.players:
|
|
room_id = rid
|
|
break
|
|
|
|
if not room_id:
|
|
return
|
|
|
|
# Отправляем событие тряски всем игрокам в комнате
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'shake_discard'
|
|
})
|
|
|
|
async def handle_reaction(self, ws: WebSocketServerProtocol, data: dict):
|
|
"""Обработка быстрой реакции - рассылаем всем игрокам в комнате"""
|
|
player_id = self.clients.get(ws)
|
|
if not player_id:
|
|
return
|
|
|
|
emoji = data.get('emoji')
|
|
if not emoji:
|
|
return
|
|
|
|
# Находим комнату игрока
|
|
room_id = None
|
|
for rid, room in self.rooms.items():
|
|
if player_id in room.players:
|
|
room_id = rid
|
|
break
|
|
|
|
if not room_id:
|
|
return
|
|
|
|
room = self.rooms[room_id]
|
|
|
|
# Отправляем реакцию всем игрокам в комнате
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'reaction',
|
|
'player_id': player_id,
|
|
'emoji': emoji
|
|
})
|
|
|
|
# Если реакция отправлена боту, один случайный бот отвечает
|
|
if player_id.startswith('bot_'):
|
|
return # Боты не отвечают на реакции других ботов
|
|
|
|
# Находим всех ботов в комнате
|
|
bots = [pid for pid in room.players if pid.startswith('bot_')]
|
|
|
|
if bots:
|
|
# Запускаем ответ бота в отдельной задаче чтобы не блокировать игру
|
|
asyncio.create_task(self.bot_reaction_response(room_id, bots))
|
|
|
|
async def bot_reaction_response(self, room_id: str, bots: list):
|
|
"""Бот отвечает на реакцию с задержкой"""
|
|
# Выбираем одного случайного бота для ответа
|
|
responding_bot = random.choice(bots)
|
|
|
|
# Список всех эмодзи для реакций
|
|
reaction_emojis = ['🙃', '🙁', '😡', '😈', '😇', '😠', '😄', '😎', '🤔', '😤',
|
|
'😐', '👍', '👎', '🤝', '🫰', '🪬', '🤯', '🤡', '🤨', '😑',
|
|
'😌', '😴', '🌚', '🐱', '🐸', '🐺', '🐊', '🐠', '🌹', '🗿',
|
|
'👁️', '💩', '🔩', '🔪', '⚔️', '🎺', '🔮', '🎲', '🎯',
|
|
'♥️', '♦️', '♣️', '♠️']
|
|
|
|
# Выбираем случайный эмодзи
|
|
bot_emoji = random.choice(reaction_emojis)
|
|
|
|
# Отправляем реакцию от бота с небольшой задержкой
|
|
await asyncio.sleep(random.uniform(1.7, 3.9))
|
|
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'reaction',
|
|
'player_id': responding_bot,
|
|
'emoji': bot_emoji
|
|
})
|
|
|
|
async def handle_leave_room(self, ws: WebSocketServerProtocol, data: dict):
|
|
"""Явный выход из комнаты"""
|
|
player_id = self.clients.get(ws)
|
|
if not player_id:
|
|
return
|
|
|
|
room_id = self.player_rooms.get(player_id)
|
|
if not room_id:
|
|
return
|
|
|
|
room = self.rooms.get(room_id)
|
|
if not room or player_id not in room.players:
|
|
return
|
|
|
|
# Удаляем игрока из комнаты
|
|
del room.players[player_id]
|
|
del self.player_rooms[player_id]
|
|
|
|
# Если это была приватная комната создателя или комната пустая - удаляем её
|
|
if (room.is_private and room.creator_id == player_id) or len(room.players) == 0:
|
|
del self.rooms[room_id]
|
|
# Удаляем из БД
|
|
await self.delete_room_from_db(room_id)
|
|
else:
|
|
# Уведомляем остальных игроков
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'player_left',
|
|
'player_id': player_id
|
|
})
|
|
|
|
# Обновляем список комнат
|
|
await self.broadcast_rooms()
|
|
|
|
async def handle_message(self, ws: WebSocketServerProtocol, message: str):
|
|
try:
|
|
data = json.loads(message)
|
|
msg_type = data.get('type')
|
|
|
|
if msg_type == 'create_room':
|
|
await self.handle_create_room(ws, data)
|
|
elif msg_type == 'create_bot_game':
|
|
await self.handle_create_bot_game(ws, data)
|
|
elif msg_type == 'join_room':
|
|
await self.handle_join_room(ws, data)
|
|
elif msg_type == 'toggle_ready':
|
|
await self.handle_toggle_ready(ws, data)
|
|
elif msg_type == 'play_card':
|
|
await self.handle_play_card(ws, data)
|
|
elif msg_type == 'draw_card':
|
|
await self.handle_draw_card(ws, data)
|
|
elif msg_type == 'skip_turn':
|
|
await self.handle_skip_turn(ws, data)
|
|
elif msg_type == 'reconnect':
|
|
await self.handle_reconnect(ws, data)
|
|
elif msg_type == 'get_rooms':
|
|
await self.broadcast_rooms()
|
|
elif msg_type == 'chat_message':
|
|
await self.handle_chat_message(ws, data)
|
|
elif msg_type == 'change_deck_size':
|
|
await self.handle_change_deck_size(ws, data)
|
|
elif msg_type == 'toggle_private':
|
|
await self.handle_toggle_private(ws, data)
|
|
elif msg_type == 'shake_discard':
|
|
await self.handle_shake_discard(ws, data)
|
|
elif msg_type == 'reaction':
|
|
await self.handle_reaction(ws, data)
|
|
elif msg_type == 'leave_room':
|
|
await self.handle_leave_room(ws, data)
|
|
|
|
except json.JSONDecodeError:
|
|
await ws.send(json.dumps({'type': 'error', 'message': 'Invalid JSON'}))
|
|
except Exception as e:
|
|
print(f"Error handling message: {e}")
|
|
await ws.send(json.dumps({'type': 'error', 'message': str(e)}))
|
|
|
|
async def handle_client(self, ws: WebSocketServerProtocol):
|
|
# Добавляем временный ID для нового клиента чтобы он получал обновления
|
|
temp_id = str(uuid.uuid4())
|
|
self.clients[ws] = temp_id
|
|
|
|
try:
|
|
# Отправляем список комнат новому клиенту
|
|
available_rooms = [
|
|
room.to_dict() for room in self.rooms.values()
|
|
if not room.game_started and all(p.score == 0 for p in room.players.values())
|
|
]
|
|
await ws.send(json.dumps({
|
|
'type': 'rooms_list',
|
|
'rooms': available_rooms
|
|
}))
|
|
|
|
async for message in ws:
|
|
await self.handle_message(ws, message)
|
|
except Exception as e:
|
|
print(f"Client error: {e}")
|
|
finally:
|
|
# Удаляем клиента при отключении
|
|
player_id = self.clients.get(ws)
|
|
if player_id:
|
|
# Если это реальный игрок (не временный ID)
|
|
if player_id != temp_id:
|
|
room_id = self.player_rooms.get(player_id)
|
|
if room_id:
|
|
room = self.rooms.get(room_id)
|
|
if room and player_id in room.players:
|
|
# Проверяем: игра идёт (game_started) ИЛИ хотя бы у одного игрока есть очки (между раундами)
|
|
game_in_progress = room.game_started or any(p.score > 0 for p in room.players.values())
|
|
|
|
# Если игра идёт - НЕ удаляем игрока из комнаты и player_rooms
|
|
if game_in_progress:
|
|
# Уведомляем других о временном отключении
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'player_disconnected',
|
|
'player_id': player_id,
|
|
'nickname': room.players[player_id].nickname
|
|
})
|
|
# Сохраняем комнату в БД
|
|
await self.save_room_to_db(room_id)
|
|
# НЕ удаляем из player_rooms - игрок сможет переподключиться
|
|
else:
|
|
# Игра не началась и ни у кого нет очков - удаляем игрока полностью
|
|
del room.players[player_id]
|
|
|
|
# Если это создатель приватной комнаты - удаляем комнату полностью
|
|
if room.is_private and room.creator_id == player_id:
|
|
# Уведомляем всех игроков что комната закрывается
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'room_closed',
|
|
'message': 'Создатель комнаты покинул игру. Комната закрыта.'
|
|
})
|
|
|
|
# Удаляем всех игроков из player_rooms
|
|
for pid in list(room.players.keys()):
|
|
if pid in self.player_rooms:
|
|
del self.player_rooms[pid]
|
|
|
|
# Удаляем комнату из памяти
|
|
del self.rooms[room_id]
|
|
|
|
# Удаляем комнату из БД
|
|
await self.db.delete_room(room_id)
|
|
else:
|
|
# Проверяем остались ли живые игроки (не боты)
|
|
human_players = [p for p in room.players.values() if not p.is_bot]
|
|
|
|
if len(human_players) == 0:
|
|
# Если остались только боты или никого - удаляем комнату
|
|
del self.rooms[room_id]
|
|
# Удаляем из БД
|
|
await self.db.delete_room(room_id)
|
|
else:
|
|
await self.broadcast_to_room(room_id, {
|
|
'type': 'player_left',
|
|
'player_id': player_id
|
|
})
|
|
|
|
await self.broadcast_rooms()
|
|
|
|
# Удаляем из player_rooms только если игра не началась
|
|
if player_id in self.player_rooms:
|
|
del self.player_rooms[player_id]
|
|
|
|
# Всегда удаляем из clients (но НЕ из player_rooms если игра идёт)
|
|
if ws in self.clients:
|
|
del self.clients[ws]
|
|
|
|
async def http_handler(request):
|
|
"""Serve static files with SPA fallback"""
|
|
path = request.path
|
|
if path == '/':
|
|
path = '/index.html'
|
|
|
|
file_path = os.path.join(os.path.dirname(__file__), 'client' + path)
|
|
|
|
# Пытаемся отдать файл
|
|
if os.path.exists(file_path) and os.path.isfile(file_path):
|
|
with open(file_path, 'rb') as f:
|
|
content = f.read()
|
|
|
|
# Определяем MIME тип
|
|
content_type = 'text/html'
|
|
if path.endswith('.css'):
|
|
content_type = 'text/css'
|
|
elif path.endswith('.js'):
|
|
content_type = 'application/javascript'
|
|
elif path.endswith('.png'):
|
|
content_type = 'image/png'
|
|
elif path.endswith('.jpg') or path.endswith('.jpeg'):
|
|
content_type = 'image/jpeg'
|
|
elif path.endswith('.svg'):
|
|
content_type = 'image/svg+xml'
|
|
elif path.endswith('.md'):
|
|
content_type = 'text/markdown'
|
|
|
|
# Заголовки для отключения кэша (для разработки)
|
|
headers = {
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
'Pragma': 'no-cache',
|
|
'Expires': '0'
|
|
}
|
|
|
|
return web.Response(body=content, content_type=content_type, headers=headers, charset='utf-8')
|
|
|
|
# SPA fallback - только для HTML маршрутов (не для статических файлов)
|
|
# Если путь содержит расширение файла - это статический файл, возвращаем 404
|
|
if '.' in path.split('/')[-1]:
|
|
print(f"Static file not found: {path}")
|
|
return web.Response(status=404, text='Not Found')
|
|
|
|
# Для маршрутов без расширения (например /room/xxx) отдаём index.html
|
|
print(f"SPA fallback for: {path}")
|
|
index_path = os.path.join(os.path.dirname(__file__), 'client/index.html')
|
|
if os.path.exists(index_path):
|
|
with open(index_path, 'rb') as f:
|
|
content = f.read()
|
|
|
|
headers = {
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
'Pragma': 'no-cache',
|
|
'Expires': '0'
|
|
}
|
|
|
|
return web.Response(body=content, content_type='text/html', headers=headers, charset='utf-8')
|
|
|
|
return web.Response(status=404, text='Not Found')
|
|
|
|
async def main(clear_rooms=False):
|
|
game_server = GameServer()
|
|
|
|
# Подключаемся к PostgreSQL
|
|
print("Connecting to PostgreSQL...")
|
|
await game_server.db.connect()
|
|
print("PostgreSQL connected successfully!")
|
|
|
|
# Если указан флаг --clearrooms, удаляем все комнаты
|
|
if clear_rooms:
|
|
print("Clearing all rooms from database...")
|
|
room_ids = await game_server.db.get_all_rooms()
|
|
for room_id in room_ids:
|
|
await game_server.db.delete_room(room_id)
|
|
print(f"Deleted {len(room_ids)} rooms from database")
|
|
else:
|
|
# Очищаем старые комнаты
|
|
await game_server.cleanup_old_rooms()
|
|
|
|
# Загружаем существующие комнаты из БД
|
|
await game_server.load_rooms_from_db()
|
|
|
|
# Запускаем задачу периодической очистки
|
|
cleanup_task = asyncio.create_task(game_server.start_cleanup_task())
|
|
|
|
# WebSocket сервер
|
|
async with serve(game_server.handle_client, "0.0.0.0", 8765):
|
|
print("WebSocket server started on ws://0.0.0.0:8765")
|
|
|
|
# HTTP сервер для статики
|
|
app = web.Application()
|
|
app.router.add_get('/{path:.*}', http_handler)
|
|
runner = web.AppRunner(app)
|
|
await runner.setup()
|
|
site = web.TCPSite(runner, '0.0.0.0', 8080)
|
|
await site.start()
|
|
|
|
print("HTTP server started on http://0.0.0.0:8080")
|
|
print("Open https://domain.com in your browser")
|
|
print("Automatic cleanup task started (runs every 6 hours)")
|
|
|
|
await asyncio.Future() # run forever
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
# Проверяем аргументы командной строки
|
|
clear_rooms = '--clearrooms' in sys.argv
|
|
|
|
if clear_rooms:
|
|
print("Starting with --clearrooms flag: all rooms will be deleted")
|
|
|
|
asyncio.run(main(clear_rooms=clear_rooms))
|