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.

Remove unused import
This commit is contained in:
Marco 2023-12-09 20:34:52 +01:00
parent e441aaec1e
commit 8f4cd2266f
12 changed files with 164 additions and 126 deletions

View File

@ -12,8 +12,8 @@ class PlayerInfo {
}); });
factory PlayerInfo.fromJson(Map<String, dynamic> json) { factory PlayerInfo.fromJson(Map<String, dynamic> json) {
final playerid = UuidValue(json['playerID']); final playerid = UuidValue.fromString(json['playerID']);
final lobbyid = UuidValue(json['lobbyID']); final lobbyid = UuidValue.fromString(json['lobbyID']);
final passphrase = json['passphrase']; final passphrase = json['passphrase'];
return PlayerInfo( return PlayerInfo(

View File

@ -1,6 +1,7 @@
import 'package:mchess/api/move.dart'; import 'package:mchess/api/move.dart';
enum MessageType { enum MessageType {
boardState,
move, move,
invalidMove, invalidMove,
colorDetermined; colorDetermined;
@ -20,7 +21,8 @@ enum ApiColor {
class ApiWebsocketMessage { class ApiWebsocketMessage {
final MessageType type; final MessageType type;
final ApiMove? move; final ApiMove? move;
final ApiColor? color; final ApiColor? turnColor;
final ApiColor? playerColor;
final String? reason; final String? reason;
final String? position; final String? position;
final ApiCoordinate? squareInCheck; final ApiCoordinate? squareInCheck;
@ -28,7 +30,8 @@ class ApiWebsocketMessage {
ApiWebsocketMessage({ ApiWebsocketMessage({
required this.type, required this.type,
required this.move, required this.move,
required this.color, required this.turnColor,
required this.playerColor,
required this.reason, required this.reason,
required this.position, required this.position,
required this.squareInCheck, required this.squareInCheck,
@ -38,34 +41,46 @@ class ApiWebsocketMessage {
final type = MessageType.fromJson(json['messageType']); final type = MessageType.fromJson(json['messageType']);
ApiWebsocketMessage ret; ApiWebsocketMessage ret;
switch (type) { 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: case MessageType.colorDetermined:
ret = ApiWebsocketMessage( ret = ApiWebsocketMessage(
type: type, type: type,
move: null, move: null,
color: ApiColor.fromJson(json['color']), turnColor: null,
reason: null, reason: null,
position: null, position: null,
squareInCheck: null, squareInCheck: null,
playerColor: ApiColor.fromJson(json['playerColor']),
); );
break; break;
case MessageType.move: case MessageType.move:
ret = ApiWebsocketMessage( ret = ApiWebsocketMessage(
type: type, type: type,
move: ApiMove.fromJson(json['move']), move: ApiMove.fromJson(json['move']),
color: null, turnColor: null,
reason: null, reason: null,
position: json['position'], position: json['position'],
squareInCheck: json['squareInCheck'] squareInCheck: json['squareInCheck'],
); playerColor: null);
break; break;
case MessageType.invalidMove: case MessageType.invalidMove:
ret = ApiWebsocketMessage( ret = ApiWebsocketMessage(
type: type, type: type,
move: ApiMove.fromJson(json['move']), move: ApiMove.fromJson(json['move']),
color: null, turnColor: null,
reason: json['reason'], reason: json['reason'],
position: null, position: null,
squareInCheck: json['squareInCheck'], squareInCheck: json['squareInCheck'],
playerColor: null,
); );
} }
return ret; return ret;
@ -74,6 +89,6 @@ class ApiWebsocketMessage {
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'messageType': type, 'messageType': type,
'move': move, 'move': move,
'color': color, 'color': turnColor,
}; };
} }

View File

@ -68,9 +68,10 @@ class _ChessSquareState extends State<ChessSquare> {
return true; return true;
}, },
listener: (context, state) { listener: (context, state) {
setState(() { setState(() {
squareColor = Colors.red; squareColor = Colors.red;
});}, });
},
child: Container( child: Container(
color: widget.color, color: widget.color,
child: ChessSquareOuterDragTarget( child: ChessSquareOuterDragTarget(

View File

@ -20,10 +20,11 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
ChessBloc._internal() : super(ChessBoardState.init()) { ChessBloc._internal() : super(ChessBoardState.init()) {
on<InitBoard>(initBoard); on<InitBoard>(initBoard);
on<ColorDetermined>(flipBoard); on<ColorDetermined>(flipBoard);
on<ReceivedMove>(moveAndPositionHandler); on<ReceivedBoardState>(moveAndPositionHandler);
on<OwnPieceMoved>(ownMoveHandler); on<OwnPieceMoved>(ownMoveHandler);
on<OwnPromotionPlayed>(ownPromotionHandler); on<OwnPromotionPlayed>(ownPromotionHandler);
on<InvalidMovePlayed>(invalidMoveHandler); on<InvalidMovePlayed>(invalidMoveHandler);
on<BoardStatusReceived>(boardStatusHandler);
} }
factory ChessBloc.getInstance() { factory ChessBloc.getInstance() {
@ -65,21 +66,24 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
} }
void moveAndPositionHandler( void moveAndPositionHandler(
ReceivedMove event, ReceivedBoardState event, Emitter<ChessBoardState> emit) {
Emitter<ChessBoardState> emit,
) {
ChessPositionManager.getInstance()
.recordMove(event.startSquare, event.endSquare, event.position);
turnColor = state.newTurnColor == ChessColor.white turnColor = state.newTurnColor == ChessColor.white
? ChessColor.black ? ChessColor.black
: ChessColor.white; : 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( emit(
ChessBoardState( ChessBoardState(
state.bottomColor, state.bottomColor,
turnColor, turnColor,
event.position, event.position,
ChessMove(from: event.startSquare, to: event.endSquare), move,
true, true,
event.squareInCheck, event.squareInCheck,
), ),
@ -93,10 +97,11 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
var apiMessage = ApiWebsocketMessage( var apiMessage = ApiWebsocketMessage(
type: MessageType.move, type: MessageType.move,
move: apiMove, move: apiMove,
color: null, turnColor: null,
reason: null, reason: null,
position: null, position: null,
squareInCheck: null, squareInCheck: null,
playerColor: null,
); );
ServerConnection.getInstance().send(jsonEncode(apiMessage)); ServerConnection.getInstance().send(jsonEncode(apiMessage));
@ -122,10 +127,11 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
var message = ApiWebsocketMessage( var message = ApiWebsocketMessage(
type: MessageType.move, type: MessageType.move,
move: apiMove, move: apiMove,
color: null, turnColor: null,
reason: null, reason: null,
position: null, position: null,
squareInCheck: null, squareInCheck: null,
playerColor: null,
); );
log(jsonEncode(message)); log(jsonEncode(message));
ServerConnection.getInstance().send(jsonEncode(message)); ServerConnection.getInstance().send(jsonEncode(message));
@ -144,6 +150,20 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
), ),
); );
} }
void boardStatusHandler(
BoardStatusReceived event, Emitter<ChessBoardState> emit) {
emit(
ChessBoardState(
event.myColor,
event.whoseTurn,
event.pos,
ChessMove.none(),
false,
ChessCoordinate.none(),
),
);
}
} }
class ChessBoardState { class ChessBoardState {
@ -167,12 +187,12 @@ class ChessBoardState {
ChessColor bottomColor, ChessColor bottomColor,
ChessColor turnColor, ChessColor turnColor,
ChessPosition position, ChessPosition position,
ChessMove lastMove, ChessMove? lastMove,
bool positionAckd, bool positionAckd,
ChessCoordinate squareInCheck, ChessCoordinate squareInCheck,
) { ) {
return ChessBoardState._(bottomColor, turnColor, position, lastMove, return ChessBoardState._(bottomColor, turnColor, position,
positionAckd, squareInCheck); lastMove ?? ChessMove.none(), positionAckd, squareInCheck);
} }
factory ChessBoardState.init() { factory ChessBoardState.init() {

View File

@ -3,13 +3,13 @@ import 'package:mchess/utils/chess_utils.dart';
abstract class ChessEvent {} abstract class ChessEvent {}
class ReceivedMove extends ChessEvent { class ReceivedBoardState extends ChessEvent {
final ChessCoordinate startSquare; final ChessCoordinate? startSquare;
final ChessCoordinate endSquare; final ChessCoordinate? endSquare;
final ChessPosition position; final ChessPosition position;
final ChessCoordinate squareInCheck; final ChessCoordinate squareInCheck;
ReceivedMove({ ReceivedBoardState({
required this.startSquare, required this.startSquare,
required this.endSquare, required this.endSquare,
required this.position, required this.position,
@ -54,3 +54,15 @@ class InvalidMovePlayed extends ChessEvent {
required this.squareInCheck, 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,
});
}

View File

@ -71,10 +71,16 @@ class ChessPositionManager {
log(logString); log(logString);
} }
void recordMove(ChessCoordinate from, ChessCoordinate to, ChessPosition pos) { void recordMove(
ChessCoordinate? from, ChessCoordinate? to, ChessPosition pos) {
position = pos; position = pos;
history.add(ChessMove(from: from, to: to)); history.add(
ChessMove(
from: from ?? ChessCoordinate.none(),
to: to ?? ChessCoordinate.none(),
),
);
logPosition(position); logPosition(position);
logHistory(history); logHistory(history);

View File

@ -12,9 +12,8 @@ import 'package:mchess/utils/chess_utils.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
class ServerConnection { class ServerConnection {
late WebSocketChannel channel; WebSocketChannel? channel;
late bool wasConnected = false; late bool wasConnected = false;
late int counter = 0;
Stream broadcast = const Stream.empty(); Stream broadcast = const Stream.empty();
static final ServerConnection _instance = ServerConnection._internal(); static final ServerConnection _instance = ServerConnection._internal();
@ -32,18 +31,22 @@ class ServerConnection {
} }
void send(String message) { void send(String message) {
channel.sink.add(message); if (channel == null) {
counter++; log("Sending on channel without initializing");
return;
}
channel!.sink.add(message);
} }
void connect(String playerID, lobbyID, String? passphrase) { void connect(String playerID, lobbyID, String? passphrase) {
String url;
if (kDebugMode) { if (kDebugMode) {
channel = url = 'ws://localhost:8080/api/ws';
WebSocketChannel.connect(Uri.parse('ws://localhost:8080/api/ws'));
} else { } else {
channel = WebSocketChannel.connect( url = 'wss://chess.sw-gross.de:9999/api/ws';
Uri.parse('wss://chess.sw-gross.de:9999/api/ws'));
} }
channel = WebSocketChannel.connect(Uri.parse(url));
send( send(
jsonEncode( jsonEncode(
WebsocketMessageIdentifyPlayer( WebsocketMessageIdentifyPlayer(
@ -54,17 +57,26 @@ class ServerConnection {
), ),
); );
log(channel.closeCode.toString()); log(channel!.closeCode.toString());
broadcast = channel.stream.asBroadcastStream(); broadcast = channel!.stream.asBroadcastStream();
broadcast.listen(handleIncomingData); broadcast.listen(handleIncomingData);
} }
void disconnectExistingConnection() {
if (channel == null) return;
channel!.sink.close();
}
void handleIncomingData(dynamic data) { void handleIncomingData(dynamic data) {
log("Data received:"); log("Data received:");
log(data); log(data);
var apiMessage = ApiWebsocketMessage.fromJson(jsonDecode(data)); var apiMessage = ApiWebsocketMessage.fromJson(jsonDecode(data));
switch (apiMessage.type) { switch (apiMessage.type) {
case MessageType.boardState:
handleBoardStateMessage(apiMessage);
break;
case MessageType.colorDetermined: case MessageType.colorDetermined:
handleIncomingColorDeterminedMessage(apiMessage); handleIncomingColorDeterminedMessage(apiMessage);
break; break;
@ -78,25 +90,46 @@ class ServerConnection {
} }
} }
void handleIncomingColorDeterminedMessage(ApiWebsocketMessage apiMessage) { void handleBoardStateMessage(ApiWebsocketMessage apiMessage) {
ConnectionCubit.getInstance().opponentConnected(); ChessMove? move;
ChessBloc.getInstance().add(InitBoard()); if (apiMessage.move != null) {
ChessBloc.getInstance().add( move = ChessMove.fromApiMove(apiMessage.move!);
ColorDetermined(myColor: ChessColor.fromApiColor(apiMessage.color!))); }
}
void handleIncomingMoveMessage(ApiWebsocketMessage apiMessage) {
var move = ChessMove.fromApiMove(apiMessage.move!);
if (apiMessage.position != null) { if (apiMessage.position != null) {
ChessBloc.getInstance().add( 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, startSquare: move.from,
endSquare: move.to, endSquare: move.to,
position: ChessPositionManager.getInstance() position: ChessPositionManager.getInstance()
.fromPGNString(apiMessage.position!), .fromPGNString(apiMessage.position!),
squareInCheck: squareInCheck: ChessCoordinate.fromApiCoordinate(
ChessCoordinate.fromApiCoordinate(apiMessage.squareInCheck ?? const ApiCoordinate(col: 0, row: 0)), apiMessage.squareInCheck ?? const ApiCoordinate(col: 0, row: 0)),
), ),
); );
} else { } else {
@ -109,8 +142,8 @@ class ServerConnection {
ChessBloc.getInstance().add( ChessBloc.getInstance().add(
InvalidMovePlayed( InvalidMovePlayed(
move: ChessMove.fromApiMove(apiMessage.move!), move: ChessMove.fromApiMove(apiMessage.move!),
squareInCheck: squareInCheck: ChessCoordinate.fromApiCoordinate(
ChessCoordinate.fromApiCoordinate(apiMessage.squareInCheck!), apiMessage.squareInCheck ?? const ApiCoordinate(col: 0, row: 0)),
), ),
); );
} }

View File

@ -5,6 +5,7 @@ import 'package:mchess/chess_bloc/chess_bloc.dart';
import 'package:mchess/chess/chess_board.dart'; import 'package:mchess/chess/chess_board.dart';
import 'package:mchess/chess_bloc/promotion_bloc.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/chess_utils.dart';
import 'package:mchess/utils/widgets/promotion_dialog.dart'; import 'package:mchess/utils/widgets/promotion_dialog.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -53,6 +54,7 @@ class _ChessGameState extends State<ChessGame> {
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {
ServerConnection.getInstance().disconnectExistingConnection();
context.push('/'); context.push('/');
}, },
child: const Icon(Icons.cancel), child: const Icon(Icons.cancel),

View File

@ -19,7 +19,9 @@ class ChessAppRouter {
GoRoute( GoRoute(
path: '/', path: '/',
name: 'lobbySelector', name: 'lobbySelector',
builder: (context, state) => const LobbySelector(), builder: (context, state) {
return const LobbySelector();
},
), ),
GoRoute( GoRoute(
path: '/prepareRandom', path: '/prepareRandom',

View File

@ -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<MoveHistory> createState() => _MoveHistoryState();
}
class _MoveHistoryState extends State<MoveHistory> {
late List<String> entries;
@override
void initState() {
entries = [];
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocListener<ChessBloc, ChessBoardState>(
listener: (context, state) {
List<String> 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),
]),
);
}
}

View File

@ -124,18 +124,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: c247a4f76071c3b97bb5ae8912968870d5565644801c5e09f3bc961b4d874895 sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.1.1" version: "12.1.3"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.2"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -212,10 +212,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6 sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.1" version: "6.0.2"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@ -361,10 +361,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: xml name: xml
sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556 sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.4.2" version: "6.5.0"
sdks: sdks:
dart: ">=3.2.0-194.0.dev <4.0.0" dart: ">=3.2.0 <4.0.0"
flutter: ">=3.7.0" flutter: ">=3.10.0"

View File

@ -13,9 +13,9 @@ import 'package:uuid/uuid.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget(const ChessGame( await tester.pumpWidget(ChessGame(
playerID: UuidValue("test"), playerID: UuidValue.fromString("test"),
lobbyID: UuidValue("testLobbyId"), lobbyID: UuidValue.fromString("testLobbyId"),
passphrase: 'test', passphrase: 'test',
)); ));