From 1cb5ffb82ba6905635a6784a2385733a486bc8fd Mon Sep 17 00:00:00 2001 From: Marco Date: Sat, 9 Dec 2023 20:34:52 +0100 Subject: [PATCH] Handle board status message This is another step to allow reconnecting after connection loss or browser closing. When the game is left with the X button on the bottom right, we will close the websocket connection, to let the server know, that we are gone. The server still has issues that prevent this from working flawlessly. --- lib/api/register.dart | 4 +- lib/api/websocket_message.dart | 39 +++++++---- lib/chess/chess_square.dart | 7 +- lib/chess_bloc/chess_bloc.dart | 44 ++++++++---- lib/chess_bloc/chess_events.dart | 20 ++++-- lib/chess_bloc/chess_position.dart | 10 ++- lib/connection/ws_connection.dart | 81 +++++++++++++++------- lib/pages/chess_game.dart | 2 + lib/utils/chess_router.dart | 5 +- lib/utils/widgets/move_history_widget.dart | 53 -------------- pubspec.lock | 20 +++--- test/widget_test.dart | 6 +- 12 files changed, 165 insertions(+), 126 deletions(-) delete mode 100644 lib/utils/widgets/move_history_widget.dart diff --git a/lib/api/register.dart b/lib/api/register.dart index b8e3a41..95b6bfa 100644 --- a/lib/api/register.dart +++ b/lib/api/register.dart @@ -12,8 +12,8 @@ class PlayerInfo { }); factory PlayerInfo.fromJson(Map json) { - final playerid = UuidValue(json['playerID']); - final lobbyid = UuidValue(json['lobbyID']); + final playerid = UuidValue.fromString(json['playerID']); + final lobbyid = UuidValue.fromString(json['lobbyID']); final passphrase = json['passphrase']; return PlayerInfo( diff --git a/lib/api/websocket_message.dart b/lib/api/websocket_message.dart index f86ab37..79df52d 100644 --- a/lib/api/websocket_message.dart +++ b/lib/api/websocket_message.dart @@ -1,6 +1,7 @@ import 'package:mchess/api/move.dart'; enum MessageType { + boardState, move, invalidMove, colorDetermined; @@ -20,7 +21,8 @@ enum ApiColor { class ApiWebsocketMessage { final MessageType type; final ApiMove? move; - final ApiColor? color; + final ApiColor? turnColor; + final ApiColor? playerColor; final String? reason; final String? position; final ApiCoordinate? squareInCheck; @@ -28,7 +30,8 @@ class ApiWebsocketMessage { ApiWebsocketMessage({ required this.type, required this.move, - required this.color, + required this.turnColor, + required this.playerColor, required this.reason, required this.position, required this.squareInCheck, @@ -38,34 +41,46 @@ class ApiWebsocketMessage { final type = MessageType.fromJson(json['messageType']); ApiWebsocketMessage ret; switch (type) { + case MessageType.boardState: + ret = ApiWebsocketMessage( + type: type, + move: null, + turnColor: ApiColor.fromJson(json['turnColor']), + reason: null, + position: json['position'], + squareInCheck: null, + playerColor: ApiColor.fromJson(json['playerColor']), + ); case MessageType.colorDetermined: ret = ApiWebsocketMessage( type: type, move: null, - color: ApiColor.fromJson(json['color']), + turnColor: null, reason: null, position: null, squareInCheck: null, + playerColor: ApiColor.fromJson(json['playerColor']), ); break; case MessageType.move: ret = ApiWebsocketMessage( - type: type, - move: ApiMove.fromJson(json['move']), - color: null, - reason: null, - position: json['position'], - squareInCheck: json['squareInCheck'] - ); + type: type, + move: ApiMove.fromJson(json['move']), + turnColor: null, + reason: null, + position: json['position'], + squareInCheck: json['squareInCheck'], + playerColor: null); break; case MessageType.invalidMove: ret = ApiWebsocketMessage( type: type, move: ApiMove.fromJson(json['move']), - color: null, + turnColor: null, reason: json['reason'], position: null, squareInCheck: json['squareInCheck'], + playerColor: null, ); } return ret; @@ -74,6 +89,6 @@ class ApiWebsocketMessage { Map toJson() => { 'messageType': type, 'move': move, - 'color': color, + 'color': turnColor, }; } diff --git a/lib/chess/chess_square.dart b/lib/chess/chess_square.dart index 256be0c..533a510 100644 --- a/lib/chess/chess_square.dart +++ b/lib/chess/chess_square.dart @@ -68,9 +68,10 @@ class _ChessSquareState extends State { return true; }, listener: (context, state) { - setState(() { - squareColor = Colors.red; - });}, + setState(() { + squareColor = Colors.red; + }); + }, child: Container( color: widget.color, child: ChessSquareOuterDragTarget( diff --git a/lib/chess_bloc/chess_bloc.dart b/lib/chess_bloc/chess_bloc.dart index 8c88a72..f419412 100644 --- a/lib/chess_bloc/chess_bloc.dart +++ b/lib/chess_bloc/chess_bloc.dart @@ -20,10 +20,11 @@ class ChessBloc extends Bloc { ChessBloc._internal() : super(ChessBoardState.init()) { on(initBoard); on(flipBoard); - on(moveAndPositionHandler); + on(moveAndPositionHandler); on(ownMoveHandler); on(ownPromotionHandler); on(invalidMoveHandler); + on(boardStatusHandler); } factory ChessBloc.getInstance() { @@ -65,21 +66,24 @@ class ChessBloc extends Bloc { } void moveAndPositionHandler( - ReceivedMove event, - Emitter emit, - ) { - ChessPositionManager.getInstance() - .recordMove(event.startSquare, event.endSquare, event.position); + ReceivedBoardState event, Emitter emit) { turnColor = state.newTurnColor == ChessColor.white ? ChessColor.black : ChessColor.white; + ChessMove? move; + if (event.startSquare != null && event.endSquare != null) { + move = ChessMove(from: event.startSquare!, to: event.endSquare!); + ChessPositionManager.getInstance() + .recordMove(event.startSquare, event.endSquare, event.position); + } + emit( ChessBoardState( state.bottomColor, turnColor, event.position, - ChessMove(from: event.startSquare, to: event.endSquare), + move, true, event.squareInCheck, ), @@ -93,10 +97,11 @@ class ChessBloc extends Bloc { var apiMessage = ApiWebsocketMessage( type: MessageType.move, move: apiMove, - color: null, + turnColor: null, reason: null, position: null, squareInCheck: null, + playerColor: null, ); ServerConnection.getInstance().send(jsonEncode(apiMessage)); @@ -122,10 +127,11 @@ class ChessBloc extends Bloc { var message = ApiWebsocketMessage( type: MessageType.move, move: apiMove, - color: null, + turnColor: null, reason: null, position: null, squareInCheck: null, + playerColor: null, ); log(jsonEncode(message)); ServerConnection.getInstance().send(jsonEncode(message)); @@ -144,6 +150,20 @@ class ChessBloc extends Bloc { ), ); } + + void boardStatusHandler( + BoardStatusReceived event, Emitter emit) { + emit( + ChessBoardState( + event.myColor, + event.whoseTurn, + event.pos, + ChessMove.none(), + false, + ChessCoordinate.none(), + ), + ); + } } class ChessBoardState { @@ -167,12 +187,12 @@ class ChessBoardState { ChessColor bottomColor, ChessColor turnColor, ChessPosition position, - ChessMove lastMove, + ChessMove? lastMove, bool positionAckd, ChessCoordinate squareInCheck, ) { - return ChessBoardState._(bottomColor, turnColor, position, lastMove, - positionAckd, squareInCheck); + return ChessBoardState._(bottomColor, turnColor, position, + lastMove ?? ChessMove.none(), positionAckd, squareInCheck); } factory ChessBoardState.init() { diff --git a/lib/chess_bloc/chess_events.dart b/lib/chess_bloc/chess_events.dart index 43c615e..f0f1ad1 100644 --- a/lib/chess_bloc/chess_events.dart +++ b/lib/chess_bloc/chess_events.dart @@ -3,13 +3,13 @@ import 'package:mchess/utils/chess_utils.dart'; abstract class ChessEvent {} -class ReceivedMove extends ChessEvent { - final ChessCoordinate startSquare; - final ChessCoordinate endSquare; +class ReceivedBoardState extends ChessEvent { + final ChessCoordinate? startSquare; + final ChessCoordinate? endSquare; final ChessPosition position; final ChessCoordinate squareInCheck; - ReceivedMove({ + ReceivedBoardState({ required this.startSquare, required this.endSquare, required this.position, @@ -54,3 +54,15 @@ class InvalidMovePlayed extends ChessEvent { required this.squareInCheck, }); } + +class BoardStatusReceived extends ChessEvent { + final ChessPosition pos; + final ChessColor myColor; + final ChessColor whoseTurn; + + BoardStatusReceived({ + required this.pos, + required this.myColor, + required this.whoseTurn, + }); +} diff --git a/lib/chess_bloc/chess_position.dart b/lib/chess_bloc/chess_position.dart index c392f74..6449930 100644 --- a/lib/chess_bloc/chess_position.dart +++ b/lib/chess_bloc/chess_position.dart @@ -71,10 +71,16 @@ class ChessPositionManager { log(logString); } - void recordMove(ChessCoordinate from, ChessCoordinate to, ChessPosition pos) { + void recordMove( + ChessCoordinate? from, ChessCoordinate? to, ChessPosition pos) { position = pos; - history.add(ChessMove(from: from, to: to)); + history.add( + ChessMove( + from: from ?? ChessCoordinate.none(), + to: to ?? ChessCoordinate.none(), + ), + ); logPosition(position); logHistory(history); diff --git a/lib/connection/ws_connection.dart b/lib/connection/ws_connection.dart index 81e16df..8b03211 100644 --- a/lib/connection/ws_connection.dart +++ b/lib/connection/ws_connection.dart @@ -12,9 +12,8 @@ import 'package:mchess/utils/chess_utils.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; class ServerConnection { - late WebSocketChannel channel; + WebSocketChannel? channel; late bool wasConnected = false; - late int counter = 0; Stream broadcast = const Stream.empty(); static final ServerConnection _instance = ServerConnection._internal(); @@ -32,18 +31,22 @@ class ServerConnection { } void send(String message) { - channel.sink.add(message); - counter++; + if (channel == null) { + log("Sending on channel without initializing"); + return; + } + channel!.sink.add(message); } void connect(String playerID, lobbyID, String? passphrase) { + String url; if (kDebugMode) { - channel = - WebSocketChannel.connect(Uri.parse('ws://localhost:8080/api/ws')); + url = 'ws://localhost:8080/api/ws'; } else { - channel = WebSocketChannel.connect( - Uri.parse('wss://chess.sw-gross.de:9999/api/ws')); + url = 'wss://chess.sw-gross.de:9999/api/ws'; } + channel = WebSocketChannel.connect(Uri.parse(url)); + send( jsonEncode( WebsocketMessageIdentifyPlayer( @@ -54,17 +57,26 @@ class ServerConnection { ), ); - log(channel.closeCode.toString()); - broadcast = channel.stream.asBroadcastStream(); + log(channel!.closeCode.toString()); + broadcast = channel!.stream.asBroadcastStream(); broadcast.listen(handleIncomingData); } + void disconnectExistingConnection() { + if (channel == null) return; + channel!.sink.close(); + } + void handleIncomingData(dynamic data) { log("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; @@ -78,25 +90,46 @@ class ServerConnection { } } - void handleIncomingColorDeterminedMessage(ApiWebsocketMessage apiMessage) { - ConnectionCubit.getInstance().opponentConnected(); - ChessBloc.getInstance().add(InitBoard()); - ChessBloc.getInstance().add( - ColorDetermined(myColor: ChessColor.fromApiColor(apiMessage.color!))); - } - - void handleIncomingMoveMessage(ApiWebsocketMessage apiMessage) { - var move = ChessMove.fromApiMove(apiMessage.move!); + void handleBoardStateMessage(ApiWebsocketMessage apiMessage) { + ChessMove? move; + if (apiMessage.move != null) { + move = ChessMove.fromApiMove(apiMessage.move!); + } if (apiMessage.position != null) { ChessBloc.getInstance().add( - ReceivedMove( + ReceivedBoardState( + startSquare: move?.from, + endSquare: move?.to, + position: ChessPositionManager.getInstance() + .fromPGNString(apiMessage.position!), + squareInCheck: ChessCoordinate.fromApiCoordinate( + apiMessage.squareInCheck ?? const ApiCoordinate(col: 0, row: 0)), + ), + ); + } else { + log('Error: no position received'); + } + } + + void handleIncomingColorDeterminedMessage(ApiWebsocketMessage apiMessage) { + ConnectionCubit.getInstance().opponentConnected(); + ChessBloc.getInstance().add(InitBoard()); + ChessBloc.getInstance().add(ColorDetermined( + myColor: ChessColor.fromApiColor(apiMessage.playerColor!))); + } + + void handleIncomingMoveMessage(ApiWebsocketMessage apiMessage) { + if (apiMessage.position != null) { + var move = ChessMove.fromApiMove(apiMessage.move!); + 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)), + squareInCheck: ChessCoordinate.fromApiCoordinate( + apiMessage.squareInCheck ?? const ApiCoordinate(col: 0, row: 0)), ), ); } else { @@ -109,8 +142,8 @@ class ServerConnection { ChessBloc.getInstance().add( InvalidMovePlayed( move: ChessMove.fromApiMove(apiMessage.move!), - squareInCheck: - ChessCoordinate.fromApiCoordinate(apiMessage.squareInCheck!), + squareInCheck: ChessCoordinate.fromApiCoordinate( + apiMessage.squareInCheck ?? const ApiCoordinate(col: 0, row: 0)), ), ); } diff --git a/lib/pages/chess_game.dart b/lib/pages/chess_game.dart index dc0180f..29b352a 100644 --- a/lib/pages/chess_game.dart +++ b/lib/pages/chess_game.dart @@ -5,6 +5,7 @@ import 'package:mchess/chess_bloc/chess_bloc.dart'; import 'package:mchess/chess/chess_board.dart'; import 'package:mchess/chess_bloc/promotion_bloc.dart'; +import 'package:mchess/connection/ws_connection.dart'; import 'package:mchess/utils/chess_utils.dart'; import 'package:mchess/utils/widgets/promotion_dialog.dart'; import 'package:uuid/uuid.dart'; @@ -53,6 +54,7 @@ class _ChessGameState extends State { ), floatingActionButton: FloatingActionButton( onPressed: () { + ServerConnection.getInstance().disconnectExistingConnection(); context.push('/'); }, child: const Icon(Icons.cancel), diff --git a/lib/utils/chess_router.dart b/lib/utils/chess_router.dart index e62cce1..44cd3a7 100644 --- a/lib/utils/chess_router.dart +++ b/lib/utils/chess_router.dart @@ -1,4 +1,5 @@ import 'package:go_router/go_router.dart'; +import 'package:mchess/connection/ws_connection.dart'; import 'package:mchess/pages/chess_game.dart'; import 'package:mchess/pages/join_game.dart'; import 'package:mchess/pages/lobby_selector.dart'; @@ -19,7 +20,9 @@ class ChessAppRouter { GoRoute( path: '/', name: 'lobbySelector', - builder: (context, state) => const LobbySelector(), + builder: (context, state) { + return const LobbySelector(); + }, ), GoRoute( path: '/prepareRandom', diff --git a/lib/utils/widgets/move_history_widget.dart b/lib/utils/widgets/move_history_widget.dart deleted file mode 100644 index 5c76587..0000000 --- a/lib/utils/widgets/move_history_widget.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:mchess/chess_bloc/chess_bloc.dart'; -import 'package:mchess/chess_bloc/chess_position.dart'; -import 'package:mchess/utils/chess_utils.dart'; - -class MoveHistory extends StatefulWidget { - const MoveHistory({super.key}); - - @override - State createState() => _MoveHistoryState(); -} - -class _MoveHistoryState extends State { - late List entries; - - @override - void initState() { - entries = []; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - List newEntries = []; - var positionManager = ChessPositionManager.getInstance(); - var allMoves = positionManager.allMoves; - - for (ChessMove move in allMoves) { - var movedPiece = positionManager.getPieceAt(move.to); - var char = pieceCharacter[ChessPieceAssetKey( - pieceClass: movedPiece!.pieceClass, - color: movedPiece.color.getOpposite())]; - - if (movedPiece.color == ChessColor.white) { - newEntries.add("$char ${move.to.toAlphabetical()}"); - } else { - newEntries.last = - "${newEntries.last}\t\t$char${move.to.toAlphabetical()}"; - } - } - setState(() { - entries = newEntries; - }); - }, - child: ListView(children: [ - for (var entry in entries) Text(entry), - ]), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index cba4d9a..9ddd967 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -124,18 +124,18 @@ packages: dependency: "direct main" description: name: go_router - sha256: c247a4f76071c3b97bb5ae8912968870d5565644801c5e09f3bc961b4d874895 + sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15 url: "https://pub.dev" source: hosted - version: "12.1.1" + version: "12.1.3" http: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" http_parser: dependency: transitive description: @@ -212,10 +212,10 @@ packages: dependency: transitive description: name: petitparser - sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" provider: dependency: transitive description: @@ -361,10 +361,10 @@ packages: dependency: transitive description: name: xml - sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556 + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.4.2" + version: "6.5.0" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" - flutter: ">=3.7.0" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/test/widget_test.dart b/test/widget_test.dart index 1d0e970..9ac2aef 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,9 +13,9 @@ import 'package:uuid/uuid.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const ChessGame( - playerID: UuidValue("test"), - lobbyID: UuidValue("testLobbyId"), + await tester.pumpWidget(ChessGame( + playerID: UuidValue.fromString("test"), + lobbyID: UuidValue.fromString("testLobbyId"), passphrase: 'test', ));