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.
This commit is contained in:
Marco 2023-12-09 20:34:52 +01:00
parent e441aaec1e
commit 1cb5ffb82b
12 changed files with 165 additions and 126 deletions

View File

@ -12,8 +12,8 @@ class PlayerInfo {
});
factory PlayerInfo.fromJson(Map<String, dynamic> 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(

View File

@ -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<String, dynamic> toJson() => {
'messageType': type,
'move': move,
'color': color,
'color': turnColor,
};
}

View File

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

View File

@ -20,10 +20,11 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
ChessBloc._internal() : super(ChessBoardState.init()) {
on<InitBoard>(initBoard);
on<ColorDetermined>(flipBoard);
on<ReceivedMove>(moveAndPositionHandler);
on<ReceivedBoardState>(moveAndPositionHandler);
on<OwnPieceMoved>(ownMoveHandler);
on<OwnPromotionPlayed>(ownPromotionHandler);
on<InvalidMovePlayed>(invalidMoveHandler);
on<BoardStatusReceived>(boardStatusHandler);
}
factory ChessBloc.getInstance() {
@ -65,21 +66,24 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
}
void moveAndPositionHandler(
ReceivedMove event,
Emitter<ChessBoardState> emit,
) {
ChessPositionManager.getInstance()
.recordMove(event.startSquare, event.endSquare, event.position);
ReceivedBoardState event, Emitter<ChessBoardState> 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<ChessEvent, ChessBoardState> {
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<ChessEvent, ChessBoardState> {
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<ChessEvent, ChessBoardState> {
),
);
}
void boardStatusHandler(
BoardStatusReceived event, Emitter<ChessBoardState> 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() {

View File

@ -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,
});
}

View File

@ -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);

View File

@ -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)),
),
);
}

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_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<ChessGame> {
),
floatingActionButton: FloatingActionButton(
onPressed: () {
ServerConnection.getInstance().disconnectExistingConnection();
context.push('/');
},
child: const Icon(Icons.cancel),

View File

@ -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',

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"
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"

View File

@ -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',
));