From 2a4da9a1af8f796ee44af00ee3ed4e59be4d5422 Mon Sep 17 00:00:00 2001 From: Marco Date: Fri, 5 Jan 2024 22:59:31 +0100 Subject: [PATCH] Implement moves by tapping the squares This adds an option to dragging-and-dropping which is slightly hard on smaller screens. Fix promotions when tapping and fix handling of subsequently tapping two pieces of your color Cancel tap if a drag is started (tapped square will not stay red in case a drag is started) Change url strategy back to the hashtag thing Change version Fix bug that would not allow a piece move if you tried to take an opponents piece. Fix the coloring of the last move after an invalid move was played. --- lib/chess/chess_app.dart | 12 +- lib/chess/chess_board.dart | 13 ++- lib/chess/chess_square.dart | 34 +++--- lib/chess/chess_square_inner_draggable.dart | 5 +- lib/chess/chess_square_outer_dragtarget.dart | 37 ++----- lib/chess_bloc/chess_bloc.dart | 34 +++--- lib/chess_bloc/chess_events.dart | 18 +-- lib/chess_bloc/tap_bloc.dart | 109 +++++++++++++++++++ lib/connection/ws_connection.dart | 7 +- lib/main.dart | 3 - lib/pages/chess_game.dart | 34 +++--- lib/pages/host_game.dart | 7 +- lib/pages/lobby_selector.dart | 7 +- lib/utils/chess_utils.dart | 23 ++++ pubspec.lock | 20 ++-- 15 files changed, 219 insertions(+), 144 deletions(-) create mode 100644 lib/chess_bloc/tap_bloc.dart diff --git a/lib/chess/chess_app.dart b/lib/chess/chess_app.dart index 2a332c8..a6866aa 100644 --- a/lib/chess/chess_app.dart +++ b/lib/chess/chess_app.dart @@ -2,6 +2,7 @@ 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/promotion_bloc.dart'; +import 'package:mchess/chess_bloc/tap_bloc.dart'; import 'package:mchess/connection_cubit/connection_cubit.dart'; import 'package:mchess/utils/chess_router.dart'; @@ -16,18 +17,21 @@ class ChessApp extends StatelessWidget { create: (_) => ConnectionCubit.getInstance(), ), BlocProvider( - create: (context) => ChessBloc.getInstance(), + create: (_) => ChessBloc.getInstance(), ), BlocProvider( - create: (context) => PromotionBloc.getInstance(), - ) + create: (_) => PromotionBloc.getInstance(), + ), + BlocProvider( + create: (_) => TapBloc.getInstance(), + ), ], child: MaterialApp.router( theme: ThemeData.dark( useMaterial3: true, ), routerConfig: ChessAppRouter.getInstance().router, - title: 'mChess 0.1.1340', + title: 'mChess 0.9.115', ), ); } diff --git a/lib/chess/chess_board.dart b/lib/chess/chess_board.dart index 21e911d..e375f6a 100644 --- a/lib/chess/chess_board.dart +++ b/lib/chess/chess_board.dart @@ -13,7 +13,8 @@ class ChessBoard extends StatelessWidget { const ChessBoard._({required this.bState, required this.squares}); - factory ChessBoard({required ChessBoardState boardState}) { + factory ChessBoard( + {required ChessBoardState boardState, ChessCoordinate? tappedSquare}) { List squares = List.empty(growable: true); for (int i = 0; i < 64; i++) { var column = (i % 8) + 1; @@ -24,16 +25,16 @@ class ChessBoard extends StatelessWidget { bool squareWasPartOfLastMove = false; if ((boardState.lastMove.to == ChessCoordinate(column, row) || boardState.lastMove.from == ChessCoordinate(column, row)) && - boardState.positionAckdByServer) { + boardState.colorLastMove) { squareWasPartOfLastMove = true; } squares.add( ChessSquare( - ChessCoordinate(column, row), - piece, - squareWasPartOfLastMove, - ), + ChessCoordinate(column, row), + piece, + squareWasPartOfLastMove, + tappedSquare == ChessCoordinate(column, row)), ); } diff --git a/lib/chess/chess_square.dart b/lib/chess/chess_square.dart index 533a510..9655669 100644 --- a/lib/chess/chess_square.dart +++ b/lib/chess/chess_square.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mchess/chess/chess_square_outer_dragtarget.dart'; -import 'package:mchess/chess_bloc/chess_bloc.dart'; +import 'package:mchess/chess_bloc/tap_bloc.dart'; import '../utils/chess_utils.dart'; class ChessSquare extends StatefulWidget { @@ -18,13 +17,18 @@ class ChessSquare extends StatefulWidget { required this.color, }); - factory ChessSquare( - ChessCoordinate coord, ChessPiece? piece, bool wasPartOfLastMove) { + factory ChessSquare(ChessCoordinate coord, ChessPiece? piece, + bool wasPartOfLastMove, bool wasTapped) { Color lightSquaresColor = wasPartOfLastMove ? Colors.green.shade200 : Colors.brown.shade50; Color darkSquaresColor = wasPartOfLastMove ? Colors.green.shade300 : Colors.brown.shade400; + if (wasTapped) { + lightSquaresColor = Colors.red.shade200; + darkSquaresColor = Colors.red.shade300; + } + Color squareColor; if (coord.row % 2 == 0) { @@ -53,31 +57,19 @@ class ChessSquare extends StatefulWidget { } class _ChessSquareState extends State { - late Color squareColor; - - @override - void initState() { - squareColor = widget.color; - super.initState(); - } - @override Widget build(BuildContext context) { - return BlocListener( - listenWhen: (previous, current) { - return true; - }, - listener: (context, state) { - setState(() { - squareColor = Colors.red; - }); - }, + return GestureDetector( child: Container( color: widget.color, child: ChessSquareOuterDragTarget( coordinate: widget.coordinate, containedPiece: widget.containedPiece ?? const ChessPiece.none()), ), + onTap: () { + TapBloc().add(SquareTappedEvent( + tapped: widget.coordinate, pieceOnSquare: widget.containedPiece)); + }, ); } } diff --git a/lib/chess/chess_square_inner_draggable.dart b/lib/chess/chess_square_inner_draggable.dart index d01e00a..58dcf73 100644 --- a/lib/chess/chess_square_inner_draggable.dart +++ b/lib/chess/chess_square_inner_draggable.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:mchess/chess/chess_square.dart'; import 'package:mchess/chess_bloc/chess_bloc.dart'; +import 'package:mchess/chess_bloc/tap_bloc.dart'; import 'package:mchess/utils/chess_utils.dart'; class ChessSquareInnerDraggable extends StatelessWidget { @@ -48,7 +49,9 @@ class ChessSquareInnerDraggable extends StatelessWidget { dragAnchorStrategy: pointerDragAnchorStrategy, child: containedPiece ?? Container(), onDragCompleted: () {}, - onDragStarted: () {}, + onDragStarted: () { + TapBloc().add(CancelTapEvent()); + }, ), ); } diff --git a/lib/chess/chess_square_outer_dragtarget.dart b/lib/chess/chess_square_outer_dragtarget.dart index 7155144..bfa8919 100644 --- a/lib/chess/chess_square_outer_dragtarget.dart +++ b/lib/chess/chess_square_outer_dragtarget.dart @@ -25,16 +25,19 @@ class ChessSquareOuterDragTarget extends StatelessWidget { // Replace the dummy value with the actual target of the move. pieceDragged.toSquare = coordinate; - if (isPromotionMove(pieceDragged)) { + if (isPromotionMove( + pieceDragged.movedPiece!.pieceClass, + ChessBloc.myColor!, + pieceDragged.toSquare, + )) { var move = ChessMove( from: pieceDragged.fromSquare, to: pieceDragged.toSquare); - PromotionBloc.getInstance().add(PawnMovedToPromotionField( + PromotionBloc().add(PawnMovedToPromotionField( move: move, colorMoved: ChessBloc.myColor!)); } else if (coordinate != pieceDragged.fromSquare) { - ChessBloc.getInstance().add(OwnPieceMoved( + ChessBloc().add(OwnPieceMoved( startSquare: pieceDragged.fromSquare, - endSquare: pieceDragged.toSquare, - piece: pieceDragged.movedPiece!)); + endSquare: pieceDragged.toSquare)); } }, builder: (context, candidateData, rejectedData) { @@ -45,28 +48,4 @@ class ChessSquareOuterDragTarget 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; - } } diff --git a/lib/chess_bloc/chess_bloc.dart b/lib/chess_bloc/chess_bloc.dart index e66e3d4..8eee842 100644 --- a/lib/chess_bloc/chess_bloc.dart +++ b/lib/chess_bloc/chess_bloc.dart @@ -24,7 +24,6 @@ class ChessBloc extends Bloc { on(ownMoveHandler); on(ownPromotionHandler); on(invalidMoveHandler); - on(boardStatusHandler); } factory ChessBloc.getInstance() { @@ -112,8 +111,14 @@ class ChessBloc extends Bloc { tempPosition[move.from] = const ChessPiece.none(); emit( - ChessBoardState(state.bottomColor, turnColor, tempPosition, move, false, - ChessCoordinate.none()), + ChessBoardState( + state.bottomColor, + turnColor, + tempPosition, + ChessPositionManager.getInstance().lastMove, + true, + ChessCoordinate.none(), + ), ); } @@ -138,31 +143,18 @@ class ChessBloc extends Bloc { void invalidMoveHandler( InvalidMovePlayed event, Emitter emit) { + var move = ChessPositionManager.getInstance().lastMove; emit( ChessBoardState( state.bottomColor, turnColor, ChessPositionManager.getInstance().currentPosition, - ChessMove.none(), - false, + move, + true, event.squareInCheck, ), ); } - - void boardStatusHandler( - BoardStatusReceived event, Emitter emit) { - emit( - ChessBoardState( - event.myColor, - event.whoseTurn, - event.pos, - ChessMove.none(), - false, - ChessCoordinate.none(), - ), - ); - } } class ChessBoardState { @@ -170,7 +162,7 @@ class ChessBoardState { final ChessColor newTurnColor; final ChessPosition position; final ChessMove lastMove; - final bool positionAckdByServer; + final bool colorLastMove; final ChessCoordinate squareInCheck; ChessBoardState._( @@ -178,7 +170,7 @@ class ChessBoardState { this.newTurnColor, this.position, this.lastMove, - this.positionAckdByServer, + this.colorLastMove, this.squareInCheck, ); diff --git a/lib/chess_bloc/chess_events.dart b/lib/chess_bloc/chess_events.dart index c657bc8..d65d1e8 100644 --- a/lib/chess_bloc/chess_events.dart +++ b/lib/chess_bloc/chess_events.dart @@ -22,12 +22,8 @@ class ReceivedBoardState extends ChessEvent { class OwnPieceMoved extends ChessEvent { final ChessCoordinate startSquare; final ChessCoordinate endSquare; - final ChessPiece piece; - OwnPieceMoved( - {required this.startSquare, - required this.endSquare, - required this.piece}); + OwnPieceMoved({required this.startSquare, required this.endSquare}); } class OwnPromotionPlayed extends ChessEvent { @@ -56,15 +52,3 @@ 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, - }); -} diff --git a/lib/chess_bloc/tap_bloc.dart b/lib/chess_bloc/tap_bloc.dart new file mode 100644 index 0000000..cf343f9 --- /dev/null +++ b/lib/chess_bloc/tap_bloc.dart @@ -0,0 +1,109 @@ +import 'dart:developer'; + +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/chess_bloc/promotion_bloc.dart'; +import 'package:mchess/utils/chess_utils.dart'; + +class TapBloc extends Bloc { + static final TapBloc _instance = TapBloc._internal(); + + static TapBloc getInstance() { + return _instance; + } + + factory TapBloc() { + return _instance; + } + + TapBloc._internal() : super(TapState.init()) { + on(handleTap); + on(cancelTap); + } + + void handleTap(SquareTappedEvent event, Emitter emit) { + ChessCoordinate? firstTappedSquare, secondTappedSquare; + ChessPiece? piece; + + if (ChessBloc.myColor != ChessBloc.turnColor) return; + + if (state.firstSquareTapped == null) { + //first tap + if (event.pieceOnSquare == null) return; + if (event.pieceOnSquare != null && + ChessBloc.myColor != event.pieceOnSquare!.color) return; + firstTappedSquare = event.tapped; + piece = event.pieceOnSquare; + } else { + //second tap + if (event.pieceOnSquare?.color == ChessBloc.myColor) { + emit(TapState( + firstSquareTapped: event.tapped, + pieceOnFirstTappedSquare: event.pieceOnSquare, + secondSquareTapped: null)); + return; + } + secondTappedSquare = event.tapped; + } + + if (state.firstSquareTapped != null && + state.firstSquareTapped != event.tapped) { + if (isPromotionMove( + state.pieceOnFirstTappedSquare!.pieceClass, + ChessBloc.myColor!, + event.tapped, + )) { + PromotionBloc().add(PawnMovedToPromotionField( + move: ChessMove(from: state.firstSquareTapped!, to: event.tapped), + colorMoved: ChessBloc.myColor!)); + emit(TapState.init()); + return; + } else { + ChessBloc().add(OwnPieceMoved( + startSquare: state.firstSquareTapped!, endSquare: event.tapped)); + emit(TapState.init()); + return; + } + } + log("handleTap() in TapBloc is called"); + emit(TapState( + firstSquareTapped: firstTappedSquare, + pieceOnFirstTappedSquare: piece, + secondSquareTapped: secondTappedSquare)); + } + + void cancelTap(CancelTapEvent event, Emitter emit) { + emit(TapState.init()); + } +} + +abstract class TapEvent {} + +class SquareTappedEvent extends TapEvent { + ChessCoordinate tapped; + ChessPiece? pieceOnSquare; + + SquareTappedEvent({required this.tapped, required this.pieceOnSquare}); +} + +class CancelTapEvent extends TapEvent {} + +class TapState { + ChessCoordinate? firstSquareTapped; + ChessPiece? pieceOnFirstTappedSquare; + + ChessCoordinate? secondSquareTapped; + + TapState( + {required this.firstSquareTapped, + required this.pieceOnFirstTappedSquare, + required this.secondSquareTapped}); + + factory TapState.init() { + return TapState( + firstSquareTapped: null, + pieceOnFirstTappedSquare: null, + secondSquareTapped: null); + } +} diff --git a/lib/connection/ws_connection.dart b/lib/connection/ws_connection.dart index b9f8240..8edac83 100644 --- a/lib/connection/ws_connection.dart +++ b/lib/connection/ws_connection.dart @@ -1,6 +1,5 @@ import 'dart:convert'; import 'dart:developer'; -import 'package:flutter/foundation.dart'; import 'package:mchess/api/move.dart'; import 'package:mchess/api/websocket_message.dart'; import 'package:mchess/chess_bloc/chess_bloc.dart'; @@ -40,11 +39,7 @@ class ServerConnection { void connect(String playerID, lobbyID, String? passphrase) { String url; - if (kDebugMode) { - url = 'ws://localhost:8080/api/ws'; - } else { - url = 'wss://chess.sw-gross.de:9999/api/ws'; - } + url = 'wss://chess.sw-gross.de:9999/api/ws'; channel = WebSocketChannel.connect(Uri.parse(url)); send( diff --git a/lib/main.dart b/lib/main.dart index a2ceaeb..5a914d7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:go_router/go_router.dart'; import 'package:mchess/chess/chess_app.dart'; void main() { GoRouter.optionURLReflectsImperativeAPIs = true; - setUrlStrategy(PathUrlStrategy()); - runApp(const ChessApp()); } diff --git a/lib/pages/chess_game.dart b/lib/pages/chess_game.dart index e7c2cdb..13f33ce 100644 --- a/lib/pages/chess_game.dart +++ b/lib/pages/chess_game.dart @@ -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/chess_bloc/tap_bloc.dart'; import 'package:mchess/utils/chess_utils.dart'; import 'package:mchess/utils/widgets/promotion_dialog.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -25,10 +26,7 @@ class ChessGame extends StatefulWidget { } class _ChessGameState extends State { - @override - void initState() { - super.initState(); - } + ChessCoordinate? tappedSquare; @override Widget build(BuildContext context) { @@ -48,18 +46,26 @@ class _ChessGameState extends State { body: Center( child: Container( margin: const EdgeInsets.all(10), - child: BlocListener( - listener: (listenerContext, state) { - if (state.showPromotionDialog) { - promotionDialogBuilder(listenerContext, state.colorMoved); - } + child: BlocListener( + listener: (context, state) { + setState(() { + tappedSquare = state.firstSquareTapped; + }); }, - child: BlocBuilder( - builder: (context, state) { - return ChessBoard( - boardState: state, - ); + child: BlocListener( + listener: (listenerContext, state) { + if (state.showPromotionDialog) { + promotionDialogBuilder(listenerContext, state.colorMoved); + } }, + child: BlocBuilder( + builder: (context, state) { + return ChessBoard( + boardState: state, + tappedSquare: tappedSquare, + ); + }, + ), ), ), ), diff --git a/lib/pages/host_game.dart b/lib/pages/host_game.dart index 9a6165c..7f36c91 100644 --- a/lib/pages/host_game.dart +++ b/lib/pages/host_game.dart @@ -1,6 +1,5 @@ import 'dart:developer'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -101,11 +100,7 @@ class _HostGameWidgetState extends State { String addr; Response response; - if (kDebugMode) { - addr = 'http://localhost:8080/api/hostPrivate'; - } else { - addr = 'https://chess.sw-gross.de:9999/api/hostPrivate'; - } + addr = 'https://chess.sw-gross.de:9999/api/hostPrivate'; try { response = await http diff --git a/lib/pages/lobby_selector.dart b/lib/pages/lobby_selector.dart index a1a4d0a..dd0f887 100644 --- a/lib/pages/lobby_selector.dart +++ b/lib/pages/lobby_selector.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:developer'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; @@ -157,11 +156,7 @@ class _LobbySelectorState extends State { var info = PlayerInfo( playerID: null, lobbyID: null, passphrase: phraseController.text); - if (kDebugMode) { - addr = 'http://localhost:8080/api/joinPrivate'; - } else { - addr = 'https://chess.sw-gross.de:9999/api/joinPrivate'; - } + addr = 'https://chess.sw-gross.de:9999/api/joinPrivate'; try { response = await http.post(Uri.parse(addr), diff --git a/lib/utils/chess_utils.dart b/lib/utils/chess_utils.dart index 43aa019..4407b6b 100644 --- a/lib/utils/chess_utils.dart +++ b/lib/utils/chess_utils.dart @@ -363,3 +363,26 @@ class PieceDragged { PieceDragged(this.fromSquare, this.toSquare, this.movedPiece); } + +bool isPromotionMove( + ChessPieceClass pieceMoved, ChessColor myColor, ChessCoordinate toSquare) { + bool isPromotion = false; + if (pieceMoved != ChessPieceClass.pawn) { + return isPromotion; + } + + switch (myColor) { + case ChessColor.black: + if (toSquare.row == 1) { + isPromotion = true; + } + break; + case ChessColor.white: + if (toSquare.row == 8) { + isPromotion = true; + } + break; + } + + return isPromotion; +} diff --git a/pubspec.lock b/pubspec.lock index b3150b7..5ca8b07 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -236,10 +236,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -324,10 +324,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: @@ -433,26 +433,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.9+2" vector_math: dependency: transitive description: