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) {
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

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