import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../core/network/ws_client.dart'; import 'game_models.dart'; /// یک بازیکن در اتاق انتظارِ میز خصوصی. class LobbyPlayer { final String name; final bool host; const LobbyPlayer(this.name, this.host); } /// وضعیت اتاق انتظارِ میز خصوصی (دورهمی). class TableLobby { final String code; final List players; final bool isHost; final int remaining; final bool unlimited; const TableLobby({ required this.code, required this.players, required this.isHost, required this.remaining, required this.unlimited, }); factory TableLobby.fromJson(Map j) => TableLobby( code: (j['code'] ?? '') as String, players: ((j['players'] as List?) ?? []) .map((e) => LobbyPlayer( (e['name'] ?? '') as String, (e['host'] ?? false) as bool)) .toList(), isHost: (j['host'] ?? false) as bool, remaining: (j['remaining'] ?? 0) as int, unlimited: (j['unlimited'] ?? false) as bool, ); String get sig => '$code|$isHost|$remaining|$unlimited|' '${players.map((p) => '${p.name}${p.host ? '*' : ''}').join(',')}'; } class GameUiState extends Equatable { final WsStatus connection; final GameState? state; final HandResult? handResult; // اوورلی نتیجه‌ی هَند (گذرا) final GameOver? gameOver; // اوورلی پایان بازی final String? notice; // پیام گذرا (خطا/خروج بازیکن) final TableLobby? lobby; // اتاق انتظارِ میز خصوصی (پیش از شروع) final int? countdown; // شمارش معکوس پیش از شروعِ بازیِ خصوصی final bool tableClosed; // میز خصوصی منحل شد (میزبان خارج شد) const GameUiState({ this.connection = WsStatus.connecting, this.state, this.handResult, this.gameOver, this.notice, this.lobby, this.countdown, this.tableClosed = false, }); GameUiState copyWith({ WsStatus? connection, GameState? state, HandResult? handResult, GameOver? gameOver, String? notice, TableLobby? lobby, int? countdown, bool? tableClosed, bool clearHandResult = false, bool clearNotice = false, }) => GameUiState( connection: connection ?? this.connection, state: state ?? this.state, handResult: clearHandResult ? null : (handResult ?? this.handResult), gameOver: gameOver ?? this.gameOver, notice: clearNotice ? null : (notice ?? this.notice), lobby: lobby ?? this.lobby, countdown: countdown ?? this.countdown, tableClosed: tableClosed ?? this.tableClosed, ); @override List get props => [ connection, state, handResult, gameOver, notice, lobby?.sig, countdown, tableClosed, ]; } class GameCubit extends Cubit { final WsClient _ws; final String tier; /// اقدامِ ورود پس از اتصال (یک‌بار). پیش‌فرض: ورود به صفِ عمومی. /// برای میز خصوصی: {'type':'create_table'} یا {'type':'join_table','code':...}. final Map _joinAction; bool _joined = false; late final StreamSubscription _msgSub; late final StreamSubscription _statusSub; GameCubit(this._ws, this.tier, {Map? joinAction}) : _joinAction = joinAction ?? {'type': 'join_queue', 'tier': tier}, super(const GameUiState()) { _msgSub = _ws.messages.listen(_onMessage); _statusSub = _ws.status.listen(_onStatus); _ws.connect(); } /// سازنده‌ی میز خصوصی: ساختِ میز جدید. GameCubit.createPrivate(WsClient ws) : this(ws, 'private', joinAction: {'type': 'create_table'}); /// سازنده‌ی میز خصوصی: پیوستن با کد. GameCubit.joinPrivate(WsClient ws, String code) : this(ws, 'private', joinAction: {'type': 'join_table', 'code': code}); void _onStatus(WsStatus s) { emit(state.copyWith(connection: s)); // اقدامِ ورود فقط یک‌بار در اولین اتصال؛ در reconnect سرور خودش بازیکن را // به میز برمی‌گرداند (نباید دوباره create/join فرستاده شود). if (s == WsStatus.connected && !_joined) { _joined = true; _ws.send(_joinAction); } } void _onMessage(Map msg) { switch (msg['type']) { case 'state': final gs = GameState.fromJson(msg); // با شروع دست/هَند جدید، اوورلی نتیجه پاک می‌شود. final clear = gs.phase == 'choose_trump' || gs.phase == 'playing'; emit(state.copyWith(state: gs, clearHandResult: clear)); case 'hand_over': emit(state.copyWith(handResult: HandResult.fromJson(msg))); case 'game_over': emit(state.copyWith(gameOver: GameOver.fromJson(msg))); case 'table_lobby': emit(state.copyWith(lobby: TableLobby.fromJson(msg))); case 'countdown': emit(state.copyWith(countdown: (msg['seconds'] ?? 3) as int)); case 'table_closed': emit(state.copyWith(tableClosed: true, notice: 'میز توسط میزبان بسته شد')); case 'player_disconnected': emit(state.copyWith(notice: 'یک بازیکن قطع شد')); case 'player_reconnected': emit(state.copyWith(notice: 'بازیکن بازگشت')); case 'player_left': emit(state.copyWith(notice: 'یک بازیکن میز را ترک کرد')); case 'error': emit(state.copyWith(notice: (msg['message'] ?? 'خطا').toString())); } } void chooseTrump(String suit) => _ws.send({'type': 'choose_trump', 'suit': suit}); void playCard(String card) => _ws.send({'type': 'play_card', 'card': card}); void leave() => _ws.send({'type': 'leave'}); void startTable() => _ws.send({'type': 'start_table'}); void leaveTable() => _ws.send({'type': 'leave_table'}); void clearNotice() => emit(state.copyWith(clearNotice: true)); @override Future close() { _msgSub.cancel(); _statusSub.cancel(); _ws.dispose(); return super.close(); } }