189 lines
6.3 KiB
Dart
189 lines
6.3 KiB
Dart
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<LobbyPlayer> 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<String, dynamic> 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<Object?> get props => [
|
|
connection,
|
|
state,
|
|
handResult,
|
|
gameOver,
|
|
notice,
|
|
lobby?.sig,
|
|
countdown,
|
|
tableClosed,
|
|
];
|
|
}
|
|
|
|
class GameCubit extends Cubit<GameUiState> {
|
|
final WsClient _ws;
|
|
final String tier;
|
|
|
|
/// اقدامِ ورود پس از اتصال (یکبار). پیشفرض: ورود به صفِ عمومی.
|
|
/// برای میز خصوصی: {'type':'create_table'} یا {'type':'join_table','code':...}.
|
|
final Map<String, dynamic> _joinAction;
|
|
bool _joined = false;
|
|
|
|
late final StreamSubscription _msgSub;
|
|
late final StreamSubscription _statusSub;
|
|
|
|
GameCubit(this._ws, this.tier, {Map<String, dynamic>? 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<String, dynamic> 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<void> close() {
|
|
_msgSub.cancel();
|
|
_statusSub.cancel();
|
|
_ws.dispose();
|
|
return super.close();
|
|
}
|
|
}
|