Merge pull request 'Handle board status message' (#2) from handle-status-reconnects into master

Reviewed-on: #2
This commit is contained in:
marco 2023-12-09 20:51:15 +01:00
commit 4a9047fd67
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',
)); ));