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.

Upgrading deps
This commit is contained in:
Marco 2024-01-05 22:59:31 +01:00
parent dfd9f09ee6
commit 212a54612c
16 changed files with 234 additions and 151 deletions

View File

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

View File

@ -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<ChessSquare> squares = List.empty(growable: true);
for (int i = 0; i < 64; i++) {
var column = (i % 8) + 1;
@ -24,7 +25,7 @@ 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;
}
@ -33,7 +34,7 @@ class ChessBoard extends StatelessWidget {
ChessCoordinate(column, row),
piece,
squareWasPartOfLastMove,
),
tappedSquare == ChessCoordinate(column, row)),
);
}

View File

@ -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<ChessSquare> {
late Color squareColor;
@override
void initState() {
squareColor = widget.color;
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocListener<ChessBloc, ChessBoardState>(
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));
},
);
}
}

View File

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

View File

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

View File

@ -24,7 +24,6 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
on<OwnPieceMoved>(ownMoveHandler);
on<OwnPromotionPlayed>(ownPromotionHandler);
on<InvalidMovePlayed>(invalidMoveHandler);
on<BoardStatusReceived>(boardStatusHandler);
}
factory ChessBloc.getInstance() {
@ -112,8 +111,14 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
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<ChessEvent, ChessBoardState> {
void invalidMoveHandler(
InvalidMovePlayed event, Emitter<ChessBoardState> 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<ChessBoardState> 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,
);

View File

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

View File

@ -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<TapEvent, TapState> {
static final TapBloc _instance = TapBloc._internal();
static TapBloc getInstance() {
return _instance;
}
factory TapBloc() {
return _instance;
}
TapBloc._internal() : super(TapState.init()) {
on<SquareTappedEvent>(handleTap);
on<CancelTapEvent>(cancelTap);
}
void handleTap(SquareTappedEvent event, Emitter<TapState> 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<TapState> 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);
}
}

View File

@ -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';
}
channel = WebSocketChannel.connect(Uri.parse(url));
send(

View File

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

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/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<ChessGame> {
@override
void initState() {
super.initState();
}
ChessCoordinate? tappedSquare;
@override
Widget build(BuildContext context) {
@ -48,6 +46,12 @@ class _ChessGameState extends State<ChessGame> {
body: Center(
child: Container(
margin: const EdgeInsets.all(10),
child: BlocListener<TapBloc, TapState>(
listener: (context, state) {
setState(() {
tappedSquare = state.firstSquareTapped;
});
},
child: BlocListener<PromotionBloc, PromotionState>(
listener: (listenerContext, state) {
if (state.showPromotionDialog) {
@ -58,12 +62,14 @@ class _ChessGameState extends State<ChessGame> {
builder: (context, state) {
return ChessBoard(
boardState: state,
tappedSquare: tappedSquare,
);
},
),
),
),
),
),
);
}

View File

@ -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<HostGameWidget> {
String addr;
Response response;
if (kDebugMode) {
addr = 'http://localhost:8080/api/hostPrivate';
} else {
addr = 'https://chess.sw-gross.de:9999/api/hostPrivate';
}
try {
response = await http

View File

@ -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<LobbySelector> {
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';
}
try {
response = await http.post(Uri.parse(addr),

View File

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

View File

@ -97,6 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@ -148,10 +156,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.0"
http_parser:
dependency: transitive
description:
@ -236,10 +244,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:
@ -308,10 +316,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
url: "https://pub.dev"
source: hosted
version: "2.3.4"
version: "2.3.5"
shared_preferences_linux:
dependency: transitive
description:
@ -324,10 +332,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:
@ -425,34 +433,34 @@ packages:
dependency: "direct main"
description:
name: uuid
sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
url: "https://pub.dev"
source: hosted
version: "4.2.2"
version: "4.3.3"
vector_graphics:
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:

View File

@ -40,7 +40,7 @@ dependencies:
cupertino_icons: ^1.0.2
flutter_bloc: ^8.1.3
quiver: ^3.1.0
web_socket_channel: 2.4.0
web_socket_channel: ^2.4.0
go_router: ^13.0.0
http: ^1.0.0
uuid: ^4.0.0