import 'dart:convert'; import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:mchess/api/move.dart'; import 'package:mchess/api/websocket_message.dart'; import 'package:mchess/chess_bloc/chess_bloc.dart'; import 'package:mchess/chess_bloc/chess_events.dart'; import 'package:mchess/api/game_info.dart'; import 'package:mchess/chess_bloc/chess_position.dart'; import 'package:mchess/connection_cubit/connection_cubit.dart'; import 'package:mchess/utils/chess_router.dart'; import 'package:mchess/utils/chess_utils.dart'; import 'package:mchess/utils/config.dart' as config; import 'package:web_socket_channel/web_socket_channel.dart'; class ServerConnection { WebSocketChannel? channel; Stream broadcast = const Stream.empty(); static final ServerConnection _instance = ServerConnection._internal(); ServerConnection._internal() { log("ServerConnection._internal constructor is called"); } factory ServerConnection() { return _instance; } factory ServerConnection.getInstance() { return ServerConnection(); } void send(String message) { if (channel == null) { log("Sending on channel without initializing"); return; } channel!.sink.add(message); } void connect(String playerID, String? passphrase) { if (channel != null) return; channel = WebSocketChannel.connect(Uri.parse(config.getWebsocketURL())); send( jsonEncode( WebsocketMessageIdentifyPlayer( playerID: (playerID), passphrase: (passphrase), ), ), ); log(channel!.closeCode.toString()); broadcast = channel!.stream.asBroadcastStream(); broadcast.listen(handleIncomingData); } void disconnectExistingConnection() { if (channel == null) return; channel!.sink.close(); channel = null; broadcast = const Stream.empty(); } void handleIncomingData(dynamic data) { log('${DateTime.now()}: Data received:'); log(data); var apiMessage = ApiWebsocketMessage.fromJson(jsonDecode(data)); switch (apiMessage.type) { case MessageType.boardState: handleBoardStateMessage(apiMessage); break; case MessageType.colorDetermined: handleIncomingColorDeterminedMessage(apiMessage); break; case MessageType.move: log('ERROR: move message received'); break; case MessageType.invalidMove: handleInvalidMoveMessage(apiMessage); case MessageType.gameEnded: handleGameEndedMessage(apiMessage); } } void handleBoardStateMessage(ApiWebsocketMessage apiMessage) { ChessMove? move; if (apiMessage.move != null) { move = ChessMove.fromApiMove(apiMessage.move!); } if (apiMessage.position != null) { ChessBloc.getInstance().add( ReceivedBoardState( startSquare: move?.from, endSquare: move?.to, position: ChessPositionManager.getInstance() .fromPGNString(apiMessage.position!), squareInCheck: ChessCoordinate.fromApiCoordinate( apiMessage.squareInCheck ?? const ApiCoordinate(col: 0, row: 0)), turnColor: ChessColor.fromApiColor(apiMessage.turnColor!), playerColor: ChessColor.fromApiColor(apiMessage.playerColor!), ), ); } else { log('Error: no position received'); } } void handleIncomingColorDeterminedMessage(ApiWebsocketMessage apiMessage) { ConnectionCubit.getInstance().opponentConnected(); ChessBloc.getInstance().add(InitBoard()); ChessBloc.getInstance().add(ColorDetermined( playerColor: ChessColor.fromApiColor(apiMessage.playerColor!))); } void handleInvalidMoveMessage(ApiWebsocketMessage apiMessage) { log("invalid move message received, with move: ${apiMessage.move.toString()}"); ChessBloc.getInstance().add( InvalidMovePlayed( move: ChessMove.fromApiMove(apiMessage.move!), squareInCheck: ChessCoordinate.fromApiCoordinate( apiMessage.squareInCheck ?? const ApiCoordinate(col: 0, row: 0)), ), ); } void handleGameEndedMessage(ApiWebsocketMessage apiMessage) { showDialog( context: navigatorKey.currentContext!, builder: (context) { String msg = ''; if (apiMessage.reason == 'whiteIsCheckmated') { msg = 'Black won! White is checkmated'; } else if (apiMessage.reason == 'blackIsCheckmated') { msg = 'White won! Black is checkmated'; } return AlertDialog( title: const Text('Game ended'), content: Text(msg), actions: [ TextButton( child: const Text('Home'), onPressed: () { navigatorKey.currentContext!.goNamed('lobbySelector'); }, ), ]); }, ); } }