Compare commits

...

10 Commits

Author SHA1 Message Date
105b6e7565 Fix two bugs
1. Fix the bug that made black move first in a new game when the old
   game was ended during blacks turn.
2. Fix bug that offered the promotion dialog to the player when a pawn
   was moved on the last rank from any square

Also, a late initializer error was fixed because the wrong move variable
was used when a pawn reached the last rank.
2023-07-11 22:29:55 +02:00
da986c8d9b Make passphrase selectable. 2023-07-06 15:01:27 +02:00
4b8624f82b Fix bug that did not change the move color when a promotion was received. 2023-07-06 00:12:03 +02:00
95fba78d0c Change behavior of promotion dialog. 2023-07-06 00:06:03 +02:00
fea24c8274 Make castling work. 2023-07-05 21:16:01 +02:00
c3d747a60e Fix position. 2023-07-03 20:05:14 +02:00
9ce188ae32 dart fix 2023-07-03 19:55:44 +02:00
a5befed62c Make promotions work. 2023-07-03 19:41:12 +02:00
0f27fc6b4e Remove unused import. 2023-07-01 09:31:48 +02:00
3bec7a84d8 Lay foundation for promotions. 2023-07-01 09:29:43 +02:00
17 changed files with 662 additions and 172 deletions

View File

@ -1,21 +1,30 @@
class ApiMove { class ApiMove {
final ApiCoordinate startSquare; final ApiCoordinate startSquare;
final ApiCoordinate endSquare; final ApiCoordinate endSquare;
String? promotionToPiece;
const ApiMove({ ApiMove({
required this.startSquare, required this.startSquare,
required this.endSquare, required this.endSquare,
this.promotionToPiece,
}); });
factory ApiMove.fromJson(Map<String, dynamic> json) { factory ApiMove.fromJson(Map<String, dynamic> json) {
final startSquare = ApiCoordinate.fromJson(json['startSquare']); final startSquare = ApiCoordinate.fromJson(json['startSquare']);
final endSquare = ApiCoordinate.fromJson(json['endSquare']); final endSquare = ApiCoordinate.fromJson(json['endSquare']);
final promotionToPiece = json['promotionToPiece'];
return ApiMove(startSquare: startSquare, endSquare: endSquare); return ApiMove(
startSquare: startSquare,
endSquare: endSquare,
promotionToPiece: promotionToPiece);
} }
Map<String, dynamic> toJson() => Map<String, dynamic> toJson() => {
{'startSquare': startSquare, 'endSquare': endSquare}; 'startSquare': startSquare,
'endSquare': endSquare,
'promotionToPiece': promotionToPiece
};
} }
class ApiCoordinate { class ApiCoordinate {

View File

@ -58,6 +58,9 @@ class ApiWebsocketMessage {
return ret; return ret;
} }
Map<String, dynamic> toJson() => Map<String, dynamic> toJson() => {
{'messageType': type, 'move': move, 'color': color}; 'messageType': type,
'move': move,
'color': color,
};
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mchess/chess_bloc/chess_bloc.dart'; import 'package:mchess/chess_bloc/chess_bloc.dart';
import 'package:mchess/chess_bloc/promotion_bloc.dart';
import 'package:mchess/connection_cubit/connection_cubit.dart'; import 'package:mchess/connection_cubit/connection_cubit.dart';
import 'package:mchess/utils/chess_router.dart'; import 'package:mchess/utils/chess_router.dart';
@ -9,17 +10,24 @@ class ChessApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return MultiBlocProvider(
create: (_) => ConnectionCubit.getInstance(), providers: [
child: BlocProvider( BlocProvider(
create: (_) => ChessBloc.getInstance(), create: (_) => ConnectionCubit.getInstance(),
child: MaterialApp.router(
theme: ThemeData.dark(
useMaterial3: true,
),
routerConfig: ChessAppRouter.getInstance().router,
title: 'mChess',
), ),
BlocProvider(
create: (context) => ChessBloc.getInstance(),
),
BlocProvider(
create: (context) => PromotionBloc.getInstance(),
)
],
child: MaterialApp.router(
theme: ThemeData.dark(
useMaterial3: true,
),
routerConfig: ChessAppRouter.getInstance().router,
title: 'mChess',
), ),
); );
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mchess/chess_bloc/chess_bloc.dart'; import 'package:mchess/chess_bloc/chess_bloc.dart';
import 'package:mchess/chess_bloc/promotion_bloc.dart';
import '../chess_bloc/chess_events.dart'; import '../chess_bloc/chess_events.dart';
import '../utils/chess_utils.dart'; import '../utils/chess_utils.dart';
@ -58,30 +59,44 @@ class ChessSquare extends StatelessWidget {
draggableFdbSize = 0.15 * windowHeight; draggableFdbSize = 0.15 * windowHeight;
} }
return DragTarget<PieceMovedFrom>( return DragTarget<PieceDragged>(
onWillAccept: (move) { onWillAccept: (move) {
if (move?.fromSquare == coordinate) { if (move?.fromSquare == coordinate) {
return false; return false;
} }
return true; return true;
}, },
onAccept: (move) { onAccept: (pieceDragged) {
if (coordinate != move.fromSquare) { // Replace the dummy value with the actual target of the move.
pieceDragged.toSquare = coordinate;
if (isPromotionMove(pieceDragged)) {
var move = ChessMove(
from: pieceDragged.fromSquare, to: pieceDragged.toSquare);
PromotionBloc.getInstance().add(PawnMovedToPromotionField(
move: move, colorMoved: ChessBloc.myColor!));
} else if (coordinate != pieceDragged.fromSquare) {
ChessBloc.getInstance().add(OwnPieceMoved( ChessBloc.getInstance().add(OwnPieceMoved(
startSquare: move.fromSquare, endSquare: coordinate)); startSquare: pieceDragged.fromSquare,
endSquare: pieceDragged.toSquare,
piece: pieceDragged.movedPiece!));
} }
}, },
builder: (context, candidateData, rejectedData) { builder: (context, candidateData, rejectedData) {
var maxDrags = var maxDrags = kDebugMode
kDebugMode ? 1 : (ChessBloc.turnColor == ChessBloc.myColor ? 1 : 0); ? 1
: ((ChessBloc.turnColor == ChessBloc.myColor) &&
(containedPiece?.color == ChessBloc.turnColor)
? 1
: 0);
return Container( return Container(
color: color, color: color,
width: ChessSquare.pieceWidth, width: ChessSquare.pieceWidth,
height: ChessSquare.pieceWidth, height: ChessSquare.pieceWidth,
child: Draggable<PieceMovedFrom>( child: Draggable<PieceDragged>(
/* We create the move with the startSquare == endSquare. The receiving widget will give the correct value to end square. */ /* We create the move with the startSquare == endSquare. The receiving widget will give the correct value to end square. */
data: PieceMovedFrom(coordinate, containedPiece), data: PieceDragged(coordinate, coordinate, containedPiece),
maxSimultaneousDrags: maxDrags, maxSimultaneousDrags: maxDrags,
feedback: FractionalTranslation( feedback: FractionalTranslation(
translation: const Offset(-0.5, -0.75), translation: const Offset(-0.5, -0.75),
@ -100,4 +115,28 @@ class ChessSquare extends StatelessWidget {
}, },
); );
} }
bool isPromotionMove(PieceDragged move) {
bool isPromotion = false;
if (move.movedPiece!.pieceClass != ChessPieceClass.pawn) {
return isPromotion;
}
switch (ChessBloc.myColor) {
case ChessColor.black:
if (move.toSquare.row == 1) {
isPromotion = true;
}
break;
case ChessColor.white:
if (move.toSquare.row == 8) {
isPromotion = true;
}
break;
case null:
break;
}
return isPromotion;
}
} }

View File

@ -1,7 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mchess/api/move.dart';
import 'package:mchess/api/websocket_message.dart'; import 'package:mchess/api/websocket_message.dart';
import 'package:mchess/chess_bloc/chess_events.dart'; import 'package:mchess/chess_bloc/chess_events.dart';
import 'package:mchess/chess_bloc/chess_position.dart'; import 'package:mchess/chess_bloc/chess_position.dart';
@ -12,7 +11,7 @@ import 'dart:developer';
class ChessBloc extends Bloc<ChessEvent, ChessBoardState> { class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
static final ChessBloc _instance = ChessBloc._internal(); static final ChessBloc _instance = ChessBloc._internal();
static ChessColor turnColor = ChessColor.white; static ChessColor turnColor = ChessColor.white;
static ChessColor? myColor; static ChessColor? myColor = ChessColor.white;
static ChessColor? getSidesColor() { static ChessColor? getSidesColor() {
return myColor; return myColor;
@ -22,7 +21,9 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
on<InitBoard>(initBoard); on<InitBoard>(initBoard);
on<ColorDetermined>(flipBoard); on<ColorDetermined>(flipBoard);
on<ReceivedMove>(moveHandler); on<ReceivedMove>(moveHandler);
on<ReceivedPromotion>(promotionHandler);
on<OwnPieceMoved>(ownMoveHandler); on<OwnPieceMoved>(ownMoveHandler);
on<OwnPromotionPlayed>(ownPromotionHandler);
on<InvalidMovePlayed>(invalidMoveHandler); on<InvalidMovePlayed>(invalidMoveHandler);
} }
@ -35,6 +36,7 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
} }
void initBoard(InitBoard event, Emitter<ChessBoardState> emit) { void initBoard(InitBoard event, Emitter<ChessBoardState> emit) {
turnColor = ChessColor.white;
ChessPosition.getInstance().resetToStartingPosition(); ChessPosition.getInstance().resetToStartingPosition();
emit(ChessBoardState(ChessColor.white, ChessColor.white, emit(ChessBoardState(ChessColor.white, ChessColor.white,
ChessPosition.getInstance().currentPosition)); ChessPosition.getInstance().currentPosition));
@ -48,9 +50,48 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
void moveHandler(ReceivedMove event, Emitter<ChessBoardState> emit) { void moveHandler(ReceivedMove event, Emitter<ChessBoardState> emit) {
log('opponentMoveHandler()'); log('opponentMoveHandler()');
var move = ChessMove(from: event.startSquare, to: event.endSquare);
bool wasEnPassant = move.wasEnPassant();
bool wasCastling = move.wasCastling();
var oldPosition = ChessPosition.getInstance().copyOfCurrentPosition;
ChessPosition.getInstance().recordMove(event.startSquare, event.endSquare); ChessPosition.getInstance().recordMove(event.startSquare, event.endSquare);
var newPosition = ChessPosition.getInstance().currentPosition; var newPosition = ChessPosition.getInstance().currentPosition;
if (wasEnPassant) {
if (turnColor == ChessColor.white) {
newPosition[ChessCoordinate(
event.endSquare.column, event.endSquare.row - 1)] =
const ChessPiece.none();
} else {
newPosition[ChessCoordinate(
event.endSquare.column, event.endSquare.row + 1)] =
const ChessPiece.none();
}
} else if (wasCastling) {
ChessPiece rookToMove;
ChessPiece kingToMove;
if (move.to.column == 7) {
rookToMove = oldPosition[ChessCoordinate(8, move.to.row)]!;
newPosition[ChessCoordinate(6, move.to.row)] = rookToMove;
newPosition[ChessCoordinate(8, move.to.row)] = const ChessPiece.none();
kingToMove = oldPosition[ChessCoordinate(5, move.to.row)]!;
newPosition[ChessCoordinate(7, move.to.row)] = kingToMove;
newPosition[ChessCoordinate(5, move.to.row)] = const ChessPiece.none();
}
if (move.to.column == 3) {
rookToMove = oldPosition[ChessCoordinate(1, move.to.row)]!;
newPosition[ChessCoordinate(4, move.to.row)] = rookToMove;
newPosition[ChessCoordinate(1, move.to.row)] = const ChessPiece.none();
kingToMove = oldPosition[ChessCoordinate(5, move.to.row)]!;
newPosition[ChessCoordinate(3, move.to.row)] = kingToMove;
newPosition[ChessCoordinate(5, move.to.row)] = const ChessPiece.none();
}
}
turnColor = state.newTurnColor == ChessColor.white turnColor = state.newTurnColor == ChessColor.white
? ChessColor.black ? ChessColor.black
: ChessColor.white; : ChessColor.white;
@ -64,14 +105,46 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
); );
} }
void promotionHandler(
ReceivedPromotion event,
Emitter<ChessBoardState> emit,
) {
var pieceAtStartSquare = ChessPosition.getInstance().getPieceAt(
ChessCoordinate(event.startSquare.column, event.startSquare.row));
if (pieceAtStartSquare == null) {
log('received a promotion but piece on start square was empty');
return;
}
ChessPieceClass pieceClass = ChessPieceClass.none;
for (var piece in chessPiecesShortName.entries) {
if (piece.value.toLowerCase() == event.piece) {
pieceClass = piece.key.pieceClass;
break;
}
}
var newPosition = ChessPosition.getInstance().currentPosition;
newPosition[
ChessCoordinate(event.startSquare.column, event.startSquare.row)] =
const ChessPiece.none();
newPosition[ChessCoordinate(event.endSquare.column, event.endSquare.row)] =
ChessPiece(pieceClass, pieceAtStartSquare.color);
turnColor = state.newTurnColor == ChessColor.white
? ChessColor.black
: ChessColor.white;
emit(ChessBoardState(
state.bottomColor,
turnColor,
newPosition,
));
}
void ownMoveHandler(OwnPieceMoved event, Emitter<ChessBoardState> emit) { void ownMoveHandler(OwnPieceMoved event, Emitter<ChessBoardState> emit) {
log('ownMoveHandler()'); log('ownMoveHandler()');
var apiMove =
var start = ApiCoordinate( ChessMove(from: event.startSquare, to: event.endSquare).toApiMove();
col: event.startSquare.column, row: event.startSquare.row);
var end =
ApiCoordinate(col: event.endSquare.column, row: event.endSquare.row);
var apiMove = ApiMove(startSquare: start, endSquare: end);
var apiMessage = ApiWebsocketMessage( var apiMessage = ApiWebsocketMessage(
type: MessageType.move, move: apiMove, color: null, reason: null); type: MessageType.move, move: apiMove, color: null, reason: null);
@ -92,10 +165,32 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
); );
} }
void ownPromotionHandler(
OwnPromotionPlayed event, Emitter<ChessBoardState> emit) {
var apiMove = event.move.toApiMove();
var shorNameForPiece = chessPiecesShortName[
ChessPieceAssetKey(pieceClass: event.pieceClass, color: myColor!)]!
.toLowerCase();
apiMove.promotionToPiece = shorNameForPiece;
var message = ApiWebsocketMessage(
type: MessageType.move,
move: apiMove,
color: null,
reason: null,
);
log(jsonEncode(message));
ServerConnection.getInstance().send(jsonEncode(message));
}
void invalidMoveHandler( void invalidMoveHandler(
InvalidMovePlayed event, Emitter<ChessBoardState> emit) { InvalidMovePlayed event, Emitter<ChessBoardState> emit) {
emit(ChessBoardState(state.bottomColor, turnColor, emit(
ChessPosition.getInstance().currentPosition)); ChessBoardState(
state.bottomColor,
turnColor,
ChessPosition.getInstance().currentPosition,
),
);
} }
} }

View File

@ -9,11 +9,33 @@ class ReceivedMove extends ChessEvent {
ReceivedMove({required this.startSquare, required this.endSquare}); ReceivedMove({required this.startSquare, required this.endSquare});
} }
class ReceivedPromotion extends ChessEvent {
final ChessCoordinate startSquare;
final ChessCoordinate endSquare;
final String piece;
ReceivedPromotion(
{required this.startSquare,
required this.endSquare,
required this.piece});
}
class OwnPieceMoved extends ChessEvent { class OwnPieceMoved extends ChessEvent {
final ChessCoordinate startSquare; final ChessCoordinate startSquare;
final ChessCoordinate endSquare; final ChessCoordinate endSquare;
final ChessPiece piece;
OwnPieceMoved({required this.startSquare, required this.endSquare}); OwnPieceMoved(
{required this.startSquare,
required this.endSquare,
required this.piece});
}
class OwnPromotionPlayed extends ChessEvent {
final ChessPieceClass pieceClass;
final ChessMove move;
OwnPromotionPlayed({required this.pieceClass, required this.move});
} }
class InitBoard extends ChessEvent { class InitBoard extends ChessEvent {

View File

@ -31,48 +31,52 @@ class ChessPosition {
for (int i = 1; i <= 8; i++) { for (int i = 1; i <= 8; i++) {
pos[ChessCoordinate(i, 7)] = pos[ChessCoordinate(i, 7)] =
ChessPiece(ChessPieceName.blackPawn, ChessColor.black); ChessPiece(ChessPieceClass.pawn, ChessColor.black);
pos[ChessCoordinate(i, 2)] = pos[ChessCoordinate(i, 2)] =
ChessPiece(ChessPieceName.whitePawn, ChessColor.white); ChessPiece(ChessPieceClass.pawn, ChessColor.white);
} }
pos[ChessCoordinate(1, 8)] = pos[ChessCoordinate(1, 8)] =
ChessPiece(ChessPieceName.blackRook, ChessColor.black); ChessPiece(ChessPieceClass.rook, ChessColor.black);
pos[ChessCoordinate(2, 8)] = pos[ChessCoordinate(2, 8)] =
ChessPiece(ChessPieceName.blackKnight, ChessColor.black); ChessPiece(ChessPieceClass.knight, ChessColor.black);
pos[ChessCoordinate(3, 8)] = pos[ChessCoordinate(3, 8)] =
ChessPiece(ChessPieceName.blackBishop, ChessColor.black); ChessPiece(ChessPieceClass.bishop, ChessColor.black);
pos[ChessCoordinate(4, 8)] = pos[ChessCoordinate(4, 8)] =
ChessPiece(ChessPieceName.blackQueen, ChessColor.black); ChessPiece(ChessPieceClass.queen, ChessColor.black);
pos[ChessCoordinate(5, 8)] = pos[ChessCoordinate(5, 8)] =
ChessPiece(ChessPieceName.blackKing, ChessColor.black); ChessPiece(ChessPieceClass.king, ChessColor.black);
pos[ChessCoordinate(6, 8)] = pos[ChessCoordinate(6, 8)] =
ChessPiece(ChessPieceName.blackBishop, ChessColor.black); ChessPiece(ChessPieceClass.bishop, ChessColor.black);
pos[ChessCoordinate(7, 8)] = pos[ChessCoordinate(7, 8)] =
ChessPiece(ChessPieceName.blackKnight, ChessColor.black); ChessPiece(ChessPieceClass.knight, ChessColor.black);
pos[ChessCoordinate(8, 8)] = pos[ChessCoordinate(8, 8)] =
ChessPiece(ChessPieceName.blackRook, ChessColor.black); ChessPiece(ChessPieceClass.rook, ChessColor.black);
pos[ChessCoordinate(1, 1)] = pos[ChessCoordinate(1, 1)] =
ChessPiece(ChessPieceName.whiteRook, ChessColor.white); ChessPiece(ChessPieceClass.rook, ChessColor.white);
pos[ChessCoordinate(2, 1)] = pos[ChessCoordinate(2, 1)] =
ChessPiece(ChessPieceName.whiteKnight, ChessColor.white); ChessPiece(ChessPieceClass.knight, ChessColor.white);
pos[ChessCoordinate(3, 1)] = pos[ChessCoordinate(3, 1)] =
ChessPiece(ChessPieceName.whiteBishop, ChessColor.white); ChessPiece(ChessPieceClass.bishop, ChessColor.white);
pos[ChessCoordinate(4, 1)] = pos[ChessCoordinate(4, 1)] =
ChessPiece(ChessPieceName.whiteQueen, ChessColor.white); ChessPiece(ChessPieceClass.queen, ChessColor.white);
pos[ChessCoordinate(5, 1)] = pos[ChessCoordinate(5, 1)] =
ChessPiece(ChessPieceName.whiteKing, ChessColor.white); ChessPiece(ChessPieceClass.king, ChessColor.white);
pos[ChessCoordinate(6, 1)] = pos[ChessCoordinate(6, 1)] =
ChessPiece(ChessPieceName.whiteBishop, ChessColor.white); ChessPiece(ChessPieceClass.bishop, ChessColor.white);
pos[ChessCoordinate(7, 1)] = pos[ChessCoordinate(7, 1)] =
ChessPiece(ChessPieceName.whiteKnight, ChessColor.white); ChessPiece(ChessPieceClass.knight, ChessColor.white);
pos[ChessCoordinate(8, 1)] = pos[ChessCoordinate(8, 1)] =
ChessPiece(ChessPieceName.whiteRook, ChessColor.white); ChessPiece(ChessPieceClass.rook, ChessColor.white);
return pos; return pos;
} }
ChessPiece? getPieceAt(ChessCoordinate coordinate) {
return position[ChessCoordinate(coordinate.column, coordinate.row)];
}
void resetToStartingPosition() { void resetToStartingPosition() {
history = ChessMoveHistory.empty(growable: true); history = ChessMoveHistory.empty(growable: true);
_instance = ChessPosition(position: _getStartingPosition()); _instance = ChessPosition(position: _getStartingPosition());

View File

@ -0,0 +1,76 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mchess/chess_bloc/chess_bloc.dart';
import 'package:mchess/chess_bloc/chess_events.dart';
import 'package:mchess/utils/chess_utils.dart';
class PromotionBloc extends Bloc<PromotionEvent, PromotionState> {
static final PromotionBloc _instance = PromotionBloc._internal();
static late ChessMove move;
PromotionBloc._internal() : super(PromotionState.init()) {
on<PawnMovedToPromotionField>(promotionMoveHandler);
on<PieceChosen>(pieceChosenHandler);
}
void promotionMoveHandler(
PawnMovedToPromotionField event,
Emitter<PromotionState> emit,
) {
switch (event.colorMoved) {
case ChessColor.white:
if (event.move.from.row != 7) return;
break;
case ChessColor.black:
if (event.move.from.row != 2) return;
break;
}
move = event.move;
emit(PromotionState(
showPromotionDialog: true, colorMoved: event.colorMoved));
}
void pieceChosenHandler(
PieceChosen event,
Emitter<PromotionState> emit,
) {
ChessBloc.getInstance()
.add(OwnPromotionPlayed(pieceClass: event.pieceClass, move: move));
emit(PromotionState(showPromotionDialog: false, colorMoved: event.color));
}
factory PromotionBloc.getInstance() {
return PromotionBloc();
}
factory PromotionBloc() {
return _instance;
}
}
abstract class PromotionEvent {}
class PawnMovedToPromotionField extends PromotionEvent {
final ChessMove move;
final ChessColor colorMoved;
PawnMovedToPromotionField({required this.move, required this.colorMoved});
}
class PieceChosen extends PromotionEvent {
final ChessPieceClass pieceClass;
final ChessColor color;
PieceChosen({required this.pieceClass, required this.color});
}
class PromotionState {
final bool showPromotionDialog;
final ChessColor colorMoved;
PromotionState({required this.showPromotionDialog, required this.colorMoved});
factory PromotionState.init() {
return PromotionState(
showPromotionDialog: false, colorMoved: ChessColor.white);
}
}

View File

@ -77,15 +77,24 @@ class ServerConnection {
} }
void handleIncomingColorDeterminedMessage(ApiWebsocketMessage apiMessage) { void handleIncomingColorDeterminedMessage(ApiWebsocketMessage apiMessage) {
ConnectionCubit.getInstance().opponentConnected();
ChessBloc.getInstance().add(InitBoard());
ChessBloc.getInstance().add( ChessBloc.getInstance().add(
ColorDetermined(myColor: ChessColor.fromApiColor(apiMessage.color!))); ColorDetermined(myColor: ChessColor.fromApiColor(apiMessage.color!)));
ConnectionCubit.getInstance().opponentConnected();
} }
void handleIncomingMoveMessage(ApiWebsocketMessage apiMessage) { void handleIncomingMoveMessage(ApiWebsocketMessage apiMessage) {
var move = ChessMove.fromApiMove(apiMessage.move!); var move = ChessMove.fromApiMove(apiMessage.move!);
ChessBloc.getInstance()
.add(ReceivedMove(startSquare: move.from, endSquare: move.to)); if (apiMessage.move?.promotionToPiece?.isNotEmpty ?? false) {
ChessBloc.getInstance().add(ReceivedPromotion(
startSquare: move.from,
endSquare: move.to,
piece: apiMessage.move!.promotionToPiece!));
} else {
ChessBloc.getInstance()
.add(ReceivedMove(startSquare: move.from, endSquare: move.to));
}
} }
void handleInvalidMoveMessage(ApiWebsocketMessage apiMessage) { void handleInvalidMoveMessage(ApiWebsocketMessage apiMessage) {

View File

@ -6,6 +6,9 @@ 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/turn_indicator_widget.dart'; import 'package:mchess/chess/turn_indicator_widget.dart';
import 'package:mchess/chess_bloc/promotion_bloc.dart';
import 'package:mchess/utils/chess_utils.dart';
import 'package:mchess/utils/widgets/promotion_dialog.dart';
import 'package:mchess/utils/widgets/server_log_widget.dart'; import 'package:mchess/utils/widgets/server_log_widget.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -32,23 +35,32 @@ class _ChessGameState extends State<ChessGame> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: FittedBox( body: Center(
fit: BoxFit.contain, child: FittedBox(
child: Column( fit: BoxFit.contain,
children: [ child: Column(
if (kDebugMode) const ServerLogWidget(textColor: Colors.white), children: [
Container( if (kDebugMode) const ServerLogWidget(textColor: Colors.white),
margin: const EdgeInsets.all(20), Container(
child: BlocBuilder<ChessBloc, ChessBoardState>( margin: const EdgeInsets.all(20),
builder: (context, state) { child: BlocListener<PromotionBloc, PromotionState>(
return ChessBoard( listener: (context, state) {
bState: state, if (state.showPromotionDialog) {
); promotionDialogBuilder(context, state.colorMoved);
}, }
},
child: BlocBuilder<ChessBloc, ChessBoardState>(
builder: (context, state) {
return ChessBoard(
bState: state,
);
},
),
),
), ),
), if (kDebugMode) const TurnIndicator(),
if (kDebugMode) const TurnIndicator(), ],
], ),
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
@ -59,6 +71,18 @@ class _ChessGameState extends State<ChessGame> {
), ),
); );
} }
Future<void> promotionDialogBuilder(BuildContext context, ChessColor color) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return PromotionDialog(
sideColor: color,
);
},
);
}
} }
class ChessGameArguments { class ChessGameArguments {

View File

@ -6,8 +6,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:mchess/api/register.dart'; import 'package:mchess/api/register.dart';
import 'package:mchess/chess_bloc/chess_bloc.dart';
import 'package:mchess/chess_bloc/chess_events.dart';
import 'package:mchess/connection_cubit/connection_cubit.dart'; import 'package:mchess/connection_cubit/connection_cubit.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
@ -27,8 +25,6 @@ class _HostGameWidgetState extends State<HostGameWidget> {
@override @override
void initState() { void initState() {
ChessBloc.getInstance().add(InitBoard());
registerResponse = hostPrivateGame(); registerResponse = hostPrivateGame();
connectToWebsocket(registerResponse); connectToWebsocket(registerResponse);
@ -83,7 +79,7 @@ class _HostGameWidgetState extends State<HostGameWidget> {
color: Theme.of(context).colorScheme.primary), color: Theme.of(context).colorScheme.primary),
), ),
const SizedBox(height: 25), const SizedBox(height: 25),
Text( SelectableText(
passphrase, passphrase,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
@ -130,7 +126,7 @@ class _HostGameWidgetState extends State<HostGameWidget> {
Future.delayed(const Duration(seconds: 2), () { Future.delayed(const Duration(seconds: 2), () {
ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
context.pop(); context.push('/'); // We go back to the lobby selector
}); });
return null; return null;
} }

View File

@ -100,7 +100,7 @@ class _JoinGameWidgetState extends State<JoinGameWidget> {
Future.delayed(const Duration(seconds: 2), () { Future.delayed(const Duration(seconds: 2), () {
ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
context.pop(); context.push('/'); // We go back to lobby selector
}); });
return null; return null;
} }

View File

@ -13,24 +13,25 @@ class LobbySelector extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
ElevatedButton( //We deactivate random lobbies for now.
onPressed: () { // ElevatedButton(
context.push('/prepareRandom'); // onPressed: () {
}, // context.push('/prepareRandom');
child: const Row( // },
mainAxisSize: MainAxisSize.min, // child: const Row(
children: [ // mainAxisSize: MainAxisSize.min,
Icon(Icons.question_mark), // children: [
SizedBox( // Icon(Icons.question_mark),
width: 10, // SizedBox(
), // width: 10,
Text('Random') // ),
], // Text('Random')
), // ],
), // ),
const SizedBox( // ),
height: 25, // const SizedBox(
), // height: 25,
// ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
_dialogBuilder(context); _dialogBuilder(context);

View File

@ -5,8 +5,6 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:mchess/api/register.dart'; import 'package:mchess/api/register.dart';
import 'package:mchess/chess_bloc/chess_bloc.dart';
import 'package:mchess/chess_bloc/chess_events.dart';
import 'package:mchess/pages/chess_game.dart'; import 'package:mchess/pages/chess_game.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
@ -24,12 +22,8 @@ class _PrepareRandomGameWidgetState extends State<PrepareRandomGameWidget> {
@override @override
void initState() { void initState() {
ChessBloc.getInstance().add(InitBoard());
randomGameResponse = registerForRandomGame(); randomGameResponse = registerForRandomGame();
goToGameWhenResponseIsHere(randomGameResponse as Future<PlayerInfo?>); goToGameWhenResponseIsHere(randomGameResponse as Future<PlayerInfo?>);
super.initState(); super.initState();
} }

View File

@ -4,20 +4,35 @@ import 'package:mchess/api/move.dart';
import 'package:mchess/api/websocket_message.dart'; import 'package:mchess/api/websocket_message.dart';
import 'package:quiver/core.dart'; import 'package:quiver/core.dart';
enum ChessPieceName { import '../chess_bloc/chess_position.dart';
enum ChessPieceClass {
none, none,
whitePawn, pawn,
whiteBishop, bishop,
whiteKnight, knight,
whiteRook, rook,
whiteQueen, queen,
whiteKing, king,
blackPawn, }
blackBishop,
blackKnight, class ChessPieceAssetKey {
blackRook, final ChessPieceClass pieceClass;
blackQueen, final ChessColor color;
blackKing,
ChessPieceAssetKey({required this.pieceClass, required this.color});
@override
bool operator ==(Object other) {
return (other is ChessPieceAssetKey &&
(pieceClass == other.pieceClass) &&
(color == other.color));
}
@override
int get hashCode {
return hash2(pieceClass, color);
}
} }
enum ChessColor { enum ChessColor {
@ -31,38 +46,124 @@ enum ChessColor {
return white; return white;
} }
} }
ChessColor getOpposite() {
if (name == 'black') {
return white;
} else {
return black;
}
}
} }
Map<ChessPieceName, String> chessPiecesAssets = { Map<ChessPieceAssetKey, String> chessPiecesAssets = {
ChessPieceName.whitePawn: 'assets/pieces/white/pawn.svg', ChessPieceAssetKey(
ChessPieceName.whiteBishop: 'assets/pieces/white/bishop.svg', pieceClass: ChessPieceClass.pawn,
ChessPieceName.whiteKnight: 'assets/pieces/white/knight.svg', color: ChessColor.white,
ChessPieceName.whiteRook: 'assets/pieces/white/rook.svg', ): 'assets/pieces/white/pawn.svg',
ChessPieceName.whiteQueen: 'assets/pieces/white/queen.svg', ChessPieceAssetKey(
ChessPieceName.whiteKing: 'assets/pieces/white/king.svg', pieceClass: ChessPieceClass.bishop,
ChessPieceName.blackPawn: 'assets/pieces/black/pawn.svg', color: ChessColor.white,
ChessPieceName.blackBishop: 'assets/pieces/black/bishop.svg', ): 'assets/pieces/white/bishop.svg',
ChessPieceName.blackKnight: 'assets/pieces/black/knight.svg', ChessPieceAssetKey(
ChessPieceName.blackRook: 'assets/pieces/black/rook.svg', pieceClass: ChessPieceClass.knight,
ChessPieceName.blackQueen: 'assets/pieces/black/queen.svg', color: ChessColor.white,
ChessPieceName.blackKing: 'assets/pieces/black/king.svg', ): 'assets/pieces/white/knight.svg',
ChessPieceName.none: 'assets/empty.svg', ChessPieceAssetKey(
pieceClass: ChessPieceClass.rook,
color: ChessColor.white,
): 'assets/pieces/white/rook.svg',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.queen,
color: ChessColor.white,
): 'assets/pieces/white/queen.svg',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.king,
color: ChessColor.white,
): 'assets/pieces/white/king.svg',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.pawn,
color: ChessColor.black,
): 'assets/pieces/black/pawn.svg',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.bishop,
color: ChessColor.black,
): 'assets/pieces/black/bishop.svg',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.knight,
color: ChessColor.black,
): 'assets/pieces/black/knight.svg',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.rook,
color: ChessColor.black,
): 'assets/pieces/black/rook.svg',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.queen,
color: ChessColor.black,
): 'assets/pieces/black/queen.svg',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.king,
color: ChessColor.black,
): 'assets/pieces/black/king.svg',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.none,
color: ChessColor.black,
): 'assets/empty.svg',
}; };
Map<ChessPieceName, String> chessPiecesShortName = { Map<ChessPieceAssetKey, String> chessPiecesShortName = {
ChessPieceName.whitePawn: 'P', ChessPieceAssetKey(
ChessPieceName.whiteBishop: 'B', pieceClass: ChessPieceClass.pawn,
ChessPieceName.whiteKnight: 'N', color: ChessColor.white,
ChessPieceName.whiteRook: 'R', ): 'P',
ChessPieceName.whiteQueen: 'Q', ChessPieceAssetKey(
ChessPieceName.whiteKing: 'K', pieceClass: ChessPieceClass.bishop,
ChessPieceName.blackPawn: 'p', color: ChessColor.white,
ChessPieceName.blackBishop: 'b', ): 'B',
ChessPieceName.blackKnight: 'n', ChessPieceAssetKey(
ChessPieceName.blackRook: 'r', pieceClass: ChessPieceClass.knight,
ChessPieceName.blackQueen: 'q', color: ChessColor.white,
ChessPieceName.blackKing: 'k', ): 'N',
ChessPieceName.none: 'X', ChessPieceAssetKey(
pieceClass: ChessPieceClass.rook,
color: ChessColor.white,
): 'R',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.queen,
color: ChessColor.white,
): 'Q',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.king,
color: ChessColor.white,
): 'K',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.pawn,
color: ChessColor.black,
): 'p',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.bishop,
color: ChessColor.black,
): 'b',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.knight,
color: ChessColor.black,
): 'n',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.rook,
color: ChessColor.black,
): 'r',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.queen,
color: ChessColor.black,
): 'q',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.king,
color: ChessColor.black,
): 'k',
ChessPieceAssetKey(
pieceClass: ChessPieceClass.none,
color: ChessColor.black,
): '-',
}; };
class ChessCoordinate { class ChessCoordinate {
@ -82,6 +183,10 @@ class ChessCoordinate {
return ChessCoordinate(apiCoordinate.col, apiCoordinate.row); return ChessCoordinate(apiCoordinate.col, apiCoordinate.row);
} }
ApiCoordinate toApiCoordinate() {
return ApiCoordinate(col: column, row: row);
}
ChessCoordinate.copyFrom(ChessCoordinate original) ChessCoordinate.copyFrom(ChessCoordinate original)
: column = original.column, : column = original.column,
row = original.row; row = original.row;
@ -140,24 +245,26 @@ class ChessCoordinate {
class ChessPiece extends StatelessWidget { class ChessPiece extends StatelessWidget {
final ChessColor color; final ChessColor color;
final ChessPieceName pieceName; final ChessPieceClass pieceClass;
final String shortName; final String shortName;
final Widget? pieceImage; final Widget? pieceImage;
const ChessPiece._( const ChessPiece._(
this.pieceName, this.color, this.pieceImage, this.shortName); this.pieceClass, this.color, this.pieceImage, this.shortName);
factory ChessPiece(ChessPieceName name, ChessColor color) { factory ChessPiece(ChessPieceClass pieceClass, ChessColor color) {
Widget? pieceImage; Widget? pieceImage;
String pieceAssetUrl = chessPiecesAssets[name]!; String pieceAssetUrl = chessPiecesAssets[
String shortName = chessPiecesShortName[name]!; ChessPieceAssetKey(pieceClass: pieceClass, color: color)]!;
String shortName = chessPiecesShortName[
ChessPieceAssetKey(pieceClass: pieceClass, color: color)]!;
pieceImage = SvgPicture.asset(pieceAssetUrl); pieceImage = SvgPicture.asset(pieceAssetUrl);
return ChessPiece._(name, color, pieceImage, shortName); return ChessPiece._(pieceClass, color, pieceImage, shortName);
} }
const ChessPiece.none({super.key}) const ChessPiece.none({super.key})
: pieceName = ChessPieceName.none, : pieceClass = ChessPieceClass.none,
color = ChessColor.white, color = ChessColor.white,
pieceImage = null, pieceImage = null,
shortName = "-"; shortName = "-";
@ -183,6 +290,13 @@ class ChessMove {
return ChessMove(from: start, to: end); return ChessMove(from: start, to: end);
} }
ApiMove toApiMove() {
var toSquare = to.toApiCoordinate();
var fromSquare = from.toApiCoordinate();
return ApiMove(
startSquare: fromSquare, endSquare: toSquare, promotionToPiece: null);
}
@override @override
operator ==(other) { operator ==(other) {
return other is ChessMove && other.from == from && other.to == to; return other is ChessMove && other.from == from && other.to == to;
@ -192,11 +306,35 @@ class ChessMove {
int get hashCode { int get hashCode {
return hash2(from, to); return hash2(from, to);
} }
bool wasEnPassant() {
var pieceMoved = ChessPosition.getInstance().getPieceAt(from);
var pieceAtEndSquare = ChessPosition.getInstance().getPieceAt(to);
if (pieceMoved != null &&
pieceMoved.pieceClass == ChessPieceClass.pawn &&
pieceAtEndSquare == null &&
from.column != to.column) {
return true;
}
return false;
}
bool wasCastling() {
var pieceMoved = ChessPosition.getInstance().getPieceAt(from);
if (pieceMoved != null && pieceMoved.pieceClass == ChessPieceClass.king) {
var colDiff = (from.column - to.column).abs();
if (colDiff == 2) {
return true;
}
}
return false;
}
} }
class PieceMovedFrom { class PieceDragged {
ChessCoordinate fromSquare; ChessCoordinate fromSquare;
ChessCoordinate toSquare;
ChessPiece? movedPiece; ChessPiece? movedPiece;
PieceMovedFrom(this.fromSquare, this.movedPiece); PieceDragged(this.fromSquare, this.toSquare, this.movedPiece);
} }

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:mchess/chess_bloc/promotion_bloc.dart';
import 'package:mchess/utils/chess_utils.dart';
class PromotionDialog extends StatelessWidget {
final ChessColor sideColor;
const PromotionDialog({required this.sideColor, super.key});
@override
Widget build(BuildContext context) {
double windowWidth = MediaQuery.of(context).size.width;
double windowHeight = MediaQuery.of(context).size.height;
double iconSize;
if (windowWidth < windowHeight) {
iconSize = 0.15 * windowWidth;
} else {
iconSize = 0.15 * windowHeight;
}
return Dialog(
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: () {
Navigator.pop(context);
pieceChosen(ChessPieceClass.queen);
},
icon: SvgPicture.asset(chessPiecesAssets[ChessPieceAssetKey(
pieceClass: ChessPieceClass.queen, color: sideColor)]!),
iconSize: 200,
),
IconButton(
onPressed: () {
Navigator.pop(context);
pieceChosen(ChessPieceClass.rook);
},
icon: SvgPicture.asset(chessPiecesAssets[ChessPieceAssetKey(
pieceClass: ChessPieceClass.rook, color: sideColor)]!),
iconSize: 100,
),
IconButton(
onPressed: () {
Navigator.pop(context);
pieceChosen(ChessPieceClass.knight);
},
icon: SvgPicture.asset(chessPiecesAssets[ChessPieceAssetKey(
pieceClass: ChessPieceClass.knight, color: sideColor)]!),
iconSize: 10,
),
IconButton(
onPressed: () {
Navigator.pop(context);
pieceChosen(ChessPieceClass.bishop);
},
icon: SvgPicture.asset(chessPiecesAssets[ChessPieceAssetKey(
pieceClass: ChessPieceClass.bishop, color: sideColor)]!),
iconSize: iconSize,
)
],
),
);
}
void pieceChosen(ChessPieceClass pieceClass) {
PromotionBloc.getInstance()
.add(PieceChosen(pieceClass: pieceClass, color: sideColor));
}
}

View File

@ -53,10 +53,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.2" version: "1.17.1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -144,6 +144,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -164,18 +172,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.16" version: "0.12.15"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" version: "0.2.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -241,10 +249,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.0" version: "1.9.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -281,10 +289,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.5.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -333,14 +341,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
web_socket_channel: web_socket_channel:
dependency: "direct main" dependency: "direct main"
description: description:
@ -358,5 +358,5 @@ packages:
source: hosted source: hosted
version: "6.3.0" version: "6.3.0"
sdks: sdks:
dart: ">=3.1.0-185.0.dev <4.0.0" dart: ">=3.0.0 <4.0.0"
flutter: ">=3.7.0-0" flutter: ">=3.7.0-0"