Compare commits

...

48 Commits

Author SHA1 Message Date
ae087a1d56 bump version and flutter pub upgrade 2024-06-27 21:57:19 +02:00
dfc7b156f1 fix handling errors 2024-06-27 21:55:54 +02:00
e5a04b02ac flutter pub upgrade 2024-05-22 00:05:14 +02:00
9c0ff492c5 Merge pull request 'Fix rejoining' (#13) from fix-rejoining into master
Reviewed-on: #13
2024-05-21 21:56:35 +00:00
fa525c2442 bump version 2024-05-21 23:47:50 +02:00
bde3d3e358 url 2024-05-21 23:47:11 +02:00
358e8a6041 more changes because it's fun 2024-05-21 18:48:06 +02:00
2a2e219c80 Wait for websocket to be disconnected before continuing 2024-05-20 17:21:25 +02:00
adf8c86692 Make many changes
1. A game is only identified by a passphrase (not a lobby id)
2. We can store multiple passphrase/playerID combinations
2024-05-20 15:34:20 +02:00
a10db3e2a2 Merge pull request 'Make games reloadable' (#12) from simplify-flow into master
Reviewed-on: #12
2024-05-19 19:48:27 +00:00
9bbde2927b Simplify flow and allow site reloads 2024-05-19 21:44:33 +02:00
c802251c9d fix url 2024-05-19 14:46:18 +02:00
e4d4b81cba flutter pub upgrade 2024-05-19 14:44:06 +02:00
d924341742 Merge pull request 'rejoinable-game' (#11) from rejoinable-game into master
Reviewed-on: #11
2024-05-19 12:41:02 +00:00
2bed5409ef flutter pub upgrade 2024-05-15 21:15:20 +02:00
544e0b22c5 Make games rejoinable
1. Disconnect websocket connection before connecting
2. store playerInfo when hosting a game to reuse it when rejoining
2024-05-15 19:44:02 +02:00
618102dd67 Merge pull request 'Fix colors and make passphrase submittable via Enter' (#10) from fix-dialog-colors into master
Reviewed-on: #10
2024-05-11 18:01:25 +00:00
32caf86f7f Fix colors and make passphrase submittable via Enter
With this change, the lobby selector gets its dark background back.
Also, now the passphrase can be submitted by pressing Enter and not only
by clicking the 'check' icon.
2024-05-11 19:58:29 +02:00
67a4be17cd bump version 2024-05-09 22:48:15 +02:00
fb42a05f72 flutter upgrade & flutter pub upgrade 2024-05-09 22:45:20 +02:00
ebab1a4c46 upgrade dependencies 2024-03-11 02:05:32 +01:00
320dd247ff Bump version 2024-02-05 10:51:56 +01:00
fd51e582af Merge pull request 'Fix snackbar in host/join dialog' (#9) from fix-dialog-snackbar into master
Reviewed-on: #9
2024-02-05 10:48:30 +01:00
9f64959498 Revert erroneous refactor 2024-02-05 10:39:01 +01:00
ba478fedca Fix snackbar in host/join dialog 2024-02-05 10:35:28 +01:00
93a84d442e bump version 2024-02-01 11:43:49 +01:00
33e79a90c9 Merge pull request 'Fix tapping on opponent's piece' (#8) from fix-tap-on-drag into master
Reviewed-on: #8
2024-02-01 11:42:18 +01:00
200393ac76 Fix tapping on opponent's piece
With the previous change of starting a tap on a drag,
we removed the GestureDetector from all squares with any pieces.

This included the opponent's pieces. Tapping and taking an opponent's
piece was not possible anymore.

This was a bug, since we still want to detect a tap and take an opponent's piece.
2024-02-01 11:37:00 +01:00
e22dc213ac Bump version 2024-02-01 11:22:25 +01:00
ecca9f07c3 Merge pull request 'Start a tap with drag' (#7) from drag-starts-tap into master
Reviewed-on: #7
2024-02-01 11:19:57 +01:00
8fd01eed1e Remove conditional_wrap 2024-02-01 11:18:58 +01:00
6882505174 Start a tap with drag
Now, only the empty squares contain a GestureDetector.
The GestureDetector is not necessary anymore in squares with pieces
because the tap is started when the drag is started.
2024-02-01 11:11:11 +01:00
c9381aaa4c Merge pull request 'Introduce checkmate screen' (#6) from checkmate-screen into master
Reviewed-on: #6
2024-01-18 17:03:30 +01:00
13bcfb1131 Introduce checkmate screen
Show checkmate screen, once the server sends the 'gameEnded' message.

Additionally, show an icon that lets users copy the passphrase to the
clipboard.
2024-01-18 16:59:33 +01:00
7d55a0e123 Merge pull request 'Implement moves by tapping the squares' (#5) from move-by-tap into master
Reviewed-on: #5
2024-01-17 20:59:31 +01:00
212a54612c 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
2024-01-17 20:58:13 +01:00
dfd9f09ee6 Update pubspec 2024-01-05 22:58:05 +01:00
cb8e98ef81 Fix building of web and linux app at the same time 2023-12-27 15:57:01 +01:00
2fe77063d9 Merge pull request 'Fix routing again' (#4) from fix-routing-again into master
Reviewed-on: #4
2023-12-27 15:49:04 +01:00
cdc0144e39 Fix routing again 2023-12-27 15:46:15 +01:00
289ec6db26 Fix turnColor handling 2023-12-25 18:08:21 +01:00
ba947ae5e4 Fix routing and move handling 2023-12-25 17:50:58 +01:00
17ac437f5b Pop promotion dialog with context.pop() 2023-12-25 02:07:03 +01:00
abf322572d Merge pull request 'Make passphrase entry a dialog instead of a page.' (#3) from store-lobby-id-and-player-id into master
Reviewed-on: #3
2023-12-23 16:47:07 +01:00
7a51e71767 Make passphrase entry a dialog instead of a page.
Additionally, we set some groundwork for storing the game data (lobby
id, player id, passphrase) in permanent storage in order to reconnect
with it later.
2023-12-23 16:44:23 +01:00
4a9047fd67 Merge pull request 'Handle board status message' (#2) from handle-status-reconnects into master
Reviewed-on: #2
2023-12-09 20:51:15 +01:00
8f4cd2266f Handle board status message
This is another step to allow reconnecting after connection loss or
browser closing.

When the game is left with the X button on the bottom right, we will
close the websocket connection, to let the server know, that we are
gone.

The server still has issues that prevent this from working flawlessly.

Remove unused import
2023-12-09 20:48:36 +01:00
e441aaec1e pub ugrade 2023-11-27 00:20:02 +01:00
34 changed files with 1282 additions and 777 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# Custom ignores
deploy-web.sh

1
devtools_options.yaml Normal file
View File

@ -0,0 +1 @@
extensions:

65
lib/api/game_info.dart Normal file
View File

@ -0,0 +1,65 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
class GameInfo {
final UuidValue? playerID;
final String? passphrase;
const GameInfo({
required this.playerID,
required this.passphrase,
});
factory GameInfo.empty() {
return const GameInfo(playerID: null, passphrase: null);
}
factory GameInfo.fromJson(Map<String, dynamic> json) {
final playerid = UuidValue.fromString(json['playerID']);
final passphrase = json['passphrase'];
return GameInfo(playerID: playerid, passphrase: passphrase);
}
Map<String, dynamic> toJson() {
String? pid;
if (playerID != null) {
pid = playerID.toString();
}
return {
'playerID': pid,
'passphrase': passphrase,
};
}
void store() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString(passphrase!, playerID.toString());
}
static Future<GameInfo?> get(String phrase) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
var playerID = prefs.getString(phrase);
if (playerID == null) return null;
return GameInfo(
playerID: UuidValue.fromString(playerID), passphrase: phrase);
}
}
class WebsocketMessageIdentifyPlayer {
final String playerID;
final String? passphrase;
const WebsocketMessageIdentifyPlayer({
required this.playerID,
required this.passphrase,
});
Map<String, dynamic> toJson() =>
{'playerID': playerID, 'passphrase': passphrase};
}

View File

@ -1,43 +0,0 @@
import 'package:uuid/uuid.dart';
class PlayerInfo {
final UuidValue? playerID;
final UuidValue? lobbyID;
final String? passphrase;
const PlayerInfo({
required this.playerID,
required this.lobbyID,
required this.passphrase,
});
factory PlayerInfo.fromJson(Map<String, dynamic> json) {
final playerid = UuidValue(json['playerID']);
final lobbyid = UuidValue(json['lobbyID']);
final passphrase = json['passphrase'];
return PlayerInfo(
playerID: playerid, lobbyID: lobbyid, passphrase: passphrase);
}
Map<String, dynamic> toJson() => {
'playerID': playerID,
'lobbyID': lobbyID,
'passphrase': passphrase,
};
}
class WebsocketMessageIdentifyPlayer {
final String playerID;
final String lobbyID;
final String? passphrase;
const WebsocketMessageIdentifyPlayer({
required this.playerID,
required this.lobbyID,
required this.passphrase,
});
Map<String, dynamic> toJson() =>
{'lobbyID': lobbyID, 'playerID': playerID, 'passphrase': passphrase};
}

View File

@ -1,9 +1,11 @@
import 'package:mchess/api/move.dart';
enum MessageType {
boardState,
move,
invalidMove,
colorDetermined;
colorDetermined,
gameEnded;
String toJson() => name;
static MessageType fromJson(String json) => values.byName(json);
@ -20,7 +22,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 +31,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 +42,56 @@ class ApiWebsocketMessage {
final type = MessageType.fromJson(json['messageType']);
ApiWebsocketMessage ret;
switch (type) {
case MessageType.boardState:
ret = ApiWebsocketMessage(
type: type,
move: ApiMove.fromJson(json['move']),
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,
);
case MessageType.gameEnded:
ret = ApiWebsocketMessage(
type: type,
move: null,
turnColor: null,
reason: json['reason'],
position: null,
squareInCheck: null,
playerColor: null,
);
}
return ret;
@ -74,6 +100,6 @@ class ApiWebsocketMessage {
Map<String, dynamic> toJson() => {
'messageType': type,
'move': move,
'color': color,
'color': turnColor,
};
}

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',
title: 'mChess 1.0.8',
),
);
}

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

View File

@ -1,7 +1,7 @@
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 +18,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,30 +58,26 @@ 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;
});},
child: Container(
color: widget.color,
child: ChessSquareOuterDragTarget(
coordinate: widget.coordinate,
containedPiece: widget.containedPiece ?? const ChessPiece.none()),
),
var dragTarget = Container(
color: widget.color,
child: ChessSquareOuterDragTarget(
coordinate: widget.coordinate,
containedPiece: widget.containedPiece ?? const ChessPiece.none()),
);
if (widget.containedPiece == null ||
widget.containedPiece!.color != ChessBloc.getMyColor()) {
return GestureDetector(
child: dragTarget,
onTap: () {
TapBloc().add(SquareTappedEvent(
tapped: widget.coordinate, pieceOnSquare: widget.containedPiece));
},
);
} else {
return dragTarget;
}
}
}

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,10 @@ class ChessSquareInnerDraggable extends StatelessWidget {
dragAnchorStrategy: pointerDragAnchorStrategy,
child: containedPiece ?? Container(),
onDragCompleted: () {},
onDragStarted: () {},
onDragStarted: () {
TapBloc().add(SquareTappedEvent(
tapped: coordinate, pieceOnSquare: containedPiece));
},
),
);
}

View File

@ -3,6 +3,7 @@ import 'package:mchess/chess/chess_square_inner_draggable.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/chess_bloc/tap_bloc.dart';
import 'package:mchess/utils/chess_utils.dart';
class ChessSquareOuterDragTarget extends StatelessWidget {
@ -10,33 +11,36 @@ class ChessSquareOuterDragTarget extends StatelessWidget {
final ChessPiece containedPiece;
const ChessSquareOuterDragTarget(
{super.key,
required this.coordinate,
required this.containedPiece});
{super.key, required this.coordinate, required this.containedPiece});
@override
Widget build(BuildContext context) {
return DragTarget<PieceDragged>(
onWillAccept: (move) {
if (move?.fromSquare == coordinate) {
onWillAcceptWithDetails: (details) {
if (details.data.fromSquare == coordinate) {
return false;
}
return true;
},
onAccept: (pieceDragged) {
onAcceptWithDetails: (details) {
// Replace the dummy value with the actual target of the move.
pieceDragged.toSquare = coordinate;
details.data.toSquare = coordinate;
if (isPromotionMove(pieceDragged)) {
TapBloc().add(CancelTapEvent());
if (isPromotionMove(
details.data.movedPiece!.pieceClass,
ChessBloc.getMyColor(),
details.data.toSquare,
)) {
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(
startSquare: pieceDragged.fromSquare,
endSquare: pieceDragged.toSquare,
piece: pieceDragged.movedPiece!));
from: details.data.fromSquare, to: details.data.toSquare);
PromotionBloc().add(PawnMovedToPromotionField(
move: move, colorMoved: ChessBloc.myColor));
} else if (coordinate != details.data.fromSquare) {
ChessBloc().add(OwnPieceMoved(
startSquare: details.data.fromSquare,
endSquare: details.data.toSquare));
}
},
builder: (context, candidateData, rejectedData) {
@ -47,28 +51,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

@ -11,16 +11,16 @@ import 'package:mchess/utils/chess_utils.dart';
class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
static final ChessBloc _instance = ChessBloc._internal();
static ChessColor turnColor = ChessColor.white;
static ChessColor? myColor = ChessColor.white;
static ChessColor myColor = ChessColor.white;
static ChessColor? getSidesColor() {
static ChessColor getMyColor() {
return myColor;
}
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);
@ -51,10 +51,12 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
void flipBoard(ColorDetermined event, Emitter<ChessBoardState> emit) {
log("My Color is $myColor");
myColor = event.myColor;
myColor = event.playerColor;
emit(
ChessBoardState(
event.myColor,
event.playerColor,
state.newTurnColor,
state.position,
ChessMove.none(),
@ -65,21 +67,24 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
}
void moveAndPositionHandler(
ReceivedMove event,
Emitter<ChessBoardState> emit,
) {
ChessPositionManager.getInstance()
.recordMove(event.startSquare, event.endSquare, event.position);
turnColor = state.newTurnColor == ChessColor.white
? ChessColor.black
: ChessColor.white;
ReceivedBoardState event, Emitter<ChessBoardState> emit) {
ChessMove? move;
if (event.startSquare != ChessCoordinate.none() &&
event.endSquare != ChessCoordinate.none()) {
move = ChessMove(from: event.startSquare!, to: event.endSquare!);
ChessPositionManager.getInstance()
.recordMove(event.startSquare, event.endSquare, event.position);
}
myColor = event.playerColor;
turnColor = event.turnColor;
emit(
ChessBoardState(
state.bottomColor,
turnColor,
myColor,
event.turnColor,
event.position,
ChessMove(from: event.startSquare, to: event.endSquare),
move,
true,
event.squareInCheck,
),
@ -93,10 +98,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));
@ -108,8 +114,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(),
),
);
}
@ -117,15 +129,16 @@ class ChessBloc extends Bloc<ChessEvent, ChessBoardState> {
OwnPromotionPlayed event, Emitter<ChessBoardState> emit) {
var apiMove = event.move.toApiMove();
var shorNameForPiece = chessPiecesShortName[
ChessPieceAssetKey(pieceClass: event.pieceClass, color: myColor!)]!;
ChessPieceAssetKey(pieceClass: event.pieceClass, color: myColor)]!;
apiMove.promotionToPiece = shorNameForPiece;
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));
@ -133,13 +146,14 @@ 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,
),
);
@ -151,7 +165,7 @@ class ChessBoardState {
final ChessColor newTurnColor;
final ChessPosition position;
final ChessMove lastMove;
final bool positionAckdByServer;
final bool colorLastMove;
final ChessCoordinate squareInCheck;
ChessBoardState._(
@ -159,7 +173,7 @@ class ChessBoardState {
this.newTurnColor,
this.position,
this.lastMove,
this.positionAckdByServer,
this.colorLastMove,
this.squareInCheck,
);
@ -167,12 +181,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,29 +3,29 @@ 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;
final ChessColor turnColor;
final ChessColor playerColor;
ReceivedMove({
ReceivedBoardState({
required this.startSquare,
required this.endSquare,
required this.position,
required this.squareInCheck,
required this.turnColor,
required this.playerColor,
});
}
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 {
@ -40,9 +40,9 @@ class InitBoard extends ChessEvent {
}
class ColorDetermined extends ChessEvent {
final ChessColor myColor;
final ChessColor playerColor;
ColorDetermined({required this.myColor});
ColorDetermined({required this.playerColor});
}
class InvalidMovePlayed extends ChessEvent {

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);
@ -89,6 +95,15 @@ class ChessPositionManager {
return _instance;
}
static ChessPosition _getDebugPostion() {
ChessPosition pos = {};
pos[ChessCoordinate(7, 7)] =
ChessPiece(ChessPieceClass.pawn, ChessColor.white);
return pos;
}
static ChessPosition _getStartingPosition() {
ChessPosition pos = {};

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,20 +1,21 @@
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:mchess/api/move.dart';
import 'package:mchess/api/websocket_message.dart';
import 'package:mchess/chess_bloc/chess_bloc.dart';
import 'package:mchess/chess_bloc/chess_events.dart';
import 'package:mchess/api/register.dart';
import 'package:mchess/api/game_info.dart';
import 'package:mchess/chess_bloc/chess_position.dart';
import 'package:mchess/connection_cubit/connection_cubit.dart';
import 'package:mchess/utils/chess_router.dart';
import 'package:mchess/utils/chess_utils.dart';
import 'package:mchess/utils/config.dart' as config;
import 'package:web_socket_channel/web_socket_channel.dart';
class ServerConnection {
late WebSocketChannel channel;
late bool wasConnected = false;
late int counter = 0;
WebSocketChannel? channel;
Stream broadcast = const Stream.empty();
static final ServerConnection _instance = ServerConnection._internal();
@ -32,71 +33,87 @@ 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) {
if (kDebugMode) {
channel =
WebSocketChannel.connect(Uri.parse('ws://localhost:8080/api/ws'));
} else {
channel = WebSocketChannel.connect(
Uri.parse('wss://chess.sw-gross.de:9999/api/ws'));
}
send(
jsonEncode(
WebsocketMessageIdentifyPlayer(
playerID: (playerID),
lobbyID: (lobbyID),
passphrase: (passphrase),
),
),
);
Future? connect(String playerID, String? passphrase) {
if (channel != null) return null;
log(channel.closeCode.toString());
broadcast = channel.stream.asBroadcastStream();
broadcast.listen(handleIncomingData);
channel = WebSocketChannel.connect(Uri.parse(config.getWebsocketURL()));
channel!.ready.then((val) {
send(
jsonEncode(
WebsocketMessageIdentifyPlayer(
playerID: (playerID),
passphrase: (passphrase),
),
),
);
log(channel!.closeCode.toString());
broadcast = channel!.stream.asBroadcastStream();
broadcast.listen(handleIncomingData);
});
return channel!.ready;
}
Future disconnectExistingConnection() async {
if (channel == null) return;
await channel!.sink.close();
channel = null;
broadcast = const Stream.empty();
}
void handleIncomingData(dynamic data) {
log("Data received:");
log('${DateTime.now()}: 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;
case MessageType.move:
handleIncomingMoveMessage(apiMessage);
log('ERROR: move message received');
break;
case MessageType.invalidMove:
handleInvalidMoveMessage(apiMessage);
case MessageType.gameEnded:
handleGameEndedMessage(apiMessage);
}
}
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(
startSquare: move.from,
endSquare: move.to,
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)),
turnColor: ChessColor.fromApiColor(apiMessage.turnColor!),
playerColor: ChessColor.fromApiColor(apiMessage.playerColor!),
),
);
} else {
@ -104,14 +121,46 @@ class ServerConnection {
}
}
void handleIncomingColorDeterminedMessage(ApiWebsocketMessage apiMessage) {
ConnectionCubit.getInstance().opponentConnected();
ChessBloc.getInstance().add(InitBoard());
ChessBloc.getInstance().add(ColorDetermined(
playerColor: ChessColor.fromApiColor(apiMessage.playerColor!)));
}
void handleInvalidMoveMessage(ApiWebsocketMessage apiMessage) {
log("invalid move message received, with move: ${apiMessage.move.toString()}");
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)),
),
);
}
void handleGameEndedMessage(ApiWebsocketMessage apiMessage) {
showDialog(
context: navigatorKey.currentContext!,
builder: (context) {
String msg = '';
if (apiMessage.reason == 'whiteIsCheckmated') {
msg = 'Black won! White is checkmated';
} else if (apiMessage.reason == 'blackIsCheckmated') {
msg = 'White won! Black is checkmated';
}
return AlertDialog(
title: const Text('Game ended'),
content: Text(msg),
actions: <Widget>[
TextButton(
child: const Text('Home'),
onPressed: () {
navigatorKey.currentContext!.goNamed('lobbySelector');
},
),
]);
},
);
}
}

View File

@ -15,21 +15,64 @@ class ConnectionCubit extends Cubit<ConnectionCubitState> {
return _instance;
}
void connect(String playerID, lobbyID, String? passphrase) {
ServerConnection.getInstance().connect(playerID, lobbyID, passphrase);
void connect(String playerID, String? passphrase) {
var connectedFuture =
ServerConnection.getInstance().connect(playerID, passphrase);
connectedFuture?.then((val) {
emit(ConnectionCubitState(
iAmConnected: true,
connectedToPhrase: passphrase,
opponentConnected: false));
});
}
void connectIfNotConnected(String playerID, String? passphrase) {
if (state.iAmConnected && state.connectedToPhrase == passphrase) {
return;
}
if (state.iAmConnected && state.connectedToPhrase != passphrase) {
disonnect().then((val) {
connect(playerID, passphrase);
});
}
connect(playerID, passphrase);
}
Future disonnect() async {
var disconnectFuture =
ServerConnection.getInstance().disconnectExistingConnection();
disconnectFuture.then(
(val) {
emit(ConnectionCubitState.init());
},
);
return disconnectFuture;
}
void opponentConnected() {
emit(ConnectionCubitState(true));
emit(ConnectionCubitState(
iAmConnected: state.iAmConnected,
connectedToPhrase: state.connectedToPhrase,
opponentConnected: true));
}
}
class ConnectionCubitState {
final bool iAmConnected;
final String? connectedToPhrase;
final bool opponentConnected;
ConnectionCubitState(this.opponentConnected);
ConnectionCubitState(
{required this.iAmConnected,
required this.connectedToPhrase,
required this.opponentConnected});
factory ConnectionCubitState.init() {
return ConnectionCubitState(false);
return ConnectionCubitState(
iAmConnected: false, connectedToPhrase: null, opponentConnected: false);
}
}

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mchess/chess/chess_app.dart';
void main() {
GoRouter.optionURLReflectsImperativeAPIs = true;
runApp(const ChessApp());
}

View File

@ -5,58 +5,64 @@ 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';
import 'package:uuid/uuid.dart';
class ChessGame extends StatefulWidget {
final UuidValue playerID;
final UuidValue lobbyID;
final String? passphrase;
const ChessGame(
{required this.playerID,
required this.lobbyID,
required this.passphrase,
super.key});
const ChessGame({super.key});
@override
State<ChessGame> createState() => _ChessGameState();
}
class _ChessGameState extends State<ChessGame> {
@override
void initState() {
super.initState();
}
ChessCoordinate? tappedSquare;
@override
Widget build(BuildContext context) {
FloatingActionButton? fltnBtn;
if (UniversalPlatform.isLinux ||
UniversalPlatform.isMacOS ||
UniversalPlatform.isWindows) {
fltnBtn = FloatingActionButton(
child: const Icon(Icons.cancel),
onPressed: () {
context.pop();
},
);
}
return Scaffold(
floatingActionButton: fltnBtn,
body: Center(
child: Container(
margin: const EdgeInsets.all(10),
child: BlocListener<PromotionBloc, PromotionState>(
child: BlocListener<TapBloc, TapState>(
listener: (context, state) {
if (state.showPromotionDialog) {
promotionDialogBuilder(context, state.colorMoved);
}
setState(() {
tappedSquare = state.firstSquareTapped;
});
},
child: BlocBuilder<ChessBloc, ChessBoardState>(
builder: (context, state) {
return ChessBoard(
boardState: state,
);
child: BlocListener<PromotionBloc, PromotionState>(
listener: (listenerContext, state) {
if (state.showPromotionDialog) {
promotionDialogBuilder(listenerContext, state.colorMoved);
}
},
child: BlocBuilder<ChessBloc, ChessBoardState>(
builder: (context, state) {
return ChessBoard(
boardState: state,
tappedSquare: tappedSquare,
);
},
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.push('/');
},
child: const Icon(Icons.cancel),
),
);
}
@ -83,4 +89,18 @@ class ChessGameArguments {
required this.playerID,
required this.passphrase,
});
bool isValid() {
try {
lobbyID.validate();
playerID.validate();
} catch (e) {
return false;
}
if (passphrase == null) return false;
if (passphrase!.isEmpty) return false;
return true;
}
}

View File

@ -0,0 +1,161 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart';
import 'package:mchess/api/game_info.dart';
import 'package:mchess/connection_cubit/connection_cubit.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:mchess/pages/chess_game.dart';
import 'package:mchess/utils/config.dart' as config;
import 'package:mchess/utils/passphrase.dart';
import 'package:universal_platform/universal_platform.dart';
class CreateGameWidget extends StatefulWidget {
const CreateGameWidget({super.key});
@override
State<CreateGameWidget> createState() => _CreateGameWidgetState();
}
class _CreateGameWidgetState extends State<CreateGameWidget> {
late Future<GameInfo?> registerResponse;
late Future disconnectFuture;
late ChessGameArguments chessGameArgs;
@override
void initState() {
super.initState();
disconnectFuture = ConnectionCubit().disonnect();
disconnectFuture.then((val) {
registerResponse = createPrivateGame();
registerResponse.then((val) {
ConnectionCubit().connectIfNotConnected(
val!.playerID.toString(),
val.passphrase,
);
});
});
}
@override
Widget build(BuildContext context) {
FloatingActionButton? fltnBtn;
if (UniversalPlatform.isLinux ||
UniversalPlatform.isMacOS ||
UniversalPlatform.isWindows) {
fltnBtn = FloatingActionButton(
child: const Icon(Icons.cancel),
onPressed: () {
context.pop();
},
);
}
return Scaffold(
floatingActionButton: fltnBtn,
body: Center(
child: FutureBuilder(
future: disconnectFuture,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return Container();
} else {
return FutureBuilder<GameInfo?>(
future: registerResponse,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const SizedBox(
height: 100,
width: 100,
child: CircularProgressIndicator(),
);
} else {
var passphrase =
snapshot.data?.passphrase ?? "no passphrase";
return BlocListener<ConnectionCubit,
ConnectionCubitState>(
listener: (context, state) {
// We wait for our opponent to connect
if (state.opponentConnected) {
//TODO: is goNamed the correct way to navigate?
context.goNamed('game', pathParameters: {
'phrase': passphrase.toURL(),
});
}
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Give this phrase to your friend and sit tight:',
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
),
const SizedBox(height: 25),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SelectableText(
passphrase,
style: const TextStyle(
fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.copy),
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: passphrase));
},
)
],
),
const SizedBox(height: 25),
const CircularProgressIndicator()
],
),
);
}
},
);
}
}),
),
);
}
Future<GameInfo?> createPrivateGame() async {
Response response;
try {
response = await http.get(Uri.parse(config.getCreateGameURL()),
headers: {"Accept": "application/json"});
} catch (e) {
log('Exception: ${e.toString()}');
const snackBar = SnackBar(
backgroundColor: Colors.amberAccent,
content: Text("mChess server is not responding. Try again or give up"),
);
Future.delayed(const Duration(seconds: 1), () {
if (!mounted) return null;
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(snackBar);
context.goNamed('lobbySelector'); // We go back to the lobby selector
});
return null;
}
if (response.statusCode == 200) {
var info = GameInfo.fromJson(jsonDecode(response.body));
info.store();
return info;
}
return null;
}
}

View File

@ -1,140 +0,0 @@
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';
import 'package:http/http.dart';
import 'package:mchess/api/register.dart';
import 'package:mchess/connection_cubit/connection_cubit.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:mchess/pages/chess_game.dart';
class HostGameWidget extends StatefulWidget {
const HostGameWidget({super.key});
@override
State<HostGameWidget> createState() => _HostGameWidgetState();
}
class _HostGameWidgetState extends State<HostGameWidget> {
late Future<PlayerInfo?> registerResponse;
late ChessGameArguments chessGameArgs;
@override
void initState() {
registerResponse = hostPrivateGame();
connectToWebsocket(registerResponse);
super.initState();
}
void connectToWebsocket(Future<PlayerInfo?> resp) {
resp.then((value) {
if (value == null) return;
chessGameArgs = ChessGameArguments(
lobbyID: value.lobbyID!,
playerID: value.playerID!,
passphrase: value.passphrase);
ConnectionCubit.getInstance().connect(
value.playerID!.uuid,
value.lobbyID!.uuid,
value.passphrase,
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FutureBuilder<PlayerInfo?>(
future: registerResponse,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const SizedBox(
height: 100,
width: 100,
child: CircularProgressIndicator(),
);
} else {
String passphrase = snapshot.data?.passphrase ?? "no passphrase";
return BlocListener<ConnectionCubit, ConnectionCubitState>(
listener: (context, state) {
// We wait for our opponent to connect
if (state.opponentConnected) {
context.push('/game', extra: chessGameArgs);
}
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Give this phrase to your friend and sit tight:',
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
),
const SizedBox(height: 25),
SelectableText(
passphrase,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 25),
const CircularProgressIndicator()
],
),
);
}
},
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.cancel),
onPressed: () {
context.push('/');
},
),
);
}
Future<PlayerInfo?> hostPrivateGame() async {
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
.get(Uri.parse(addr), headers: {"Accept": "application/json"});
} catch (e) {
log(e.toString());
if (!context.mounted) return null;
const snackBar = SnackBar(
backgroundColor: Colors.amberAccent,
content: Text("mChess server is not responding. Try again or give up"),
);
Future.delayed(const Duration(seconds: 2), () {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(snackBar);
context.push('/'); // We go back to the lobby selector
});
return null;
}
if (response.statusCode == 200) {
log(response.body);
return PlayerInfo.fromJson(jsonDecode(response.body));
}
return null;
}
}

View File

@ -1,118 +0,0 @@
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;
import 'package:http/http.dart';
import 'package:mchess/api/register.dart';
import 'package:mchess/connection_cubit/connection_cubit.dart';
import 'package:mchess/pages/chess_game.dart';
class JoinGameWidget extends StatefulWidget {
const JoinGameWidget({
super.key,
});
@override
State<JoinGameWidget> createState() => _JoinGameWidgetState();
}
class _JoinGameWidgetState extends State<JoinGameWidget> {
final myController = TextEditingController();
late Future<PlayerInfo?> joinGameFuture;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: TextField(
controller: myController,
decoration: InputDecoration(
hintText: 'Enter passphrase here',
suffixIcon: IconButton(
onPressed: () {
joinGameFuture = joinPrivateGame();
switchToGame(joinGameFuture);
},
icon: const Icon(Icons.check),
)),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.push('/');
},
child: const Icon(Icons.cancel),
),
);
}
void switchToGame(Future<PlayerInfo?> resp) {
resp.then((value) {
if (value == null) return;
var chessGameArgs = ChessGameArguments(
lobbyID: value.lobbyID!,
playerID: value.playerID!,
passphrase: value.passphrase);
ConnectionCubit.getInstance().connect(
value.playerID!.uuid,
value.lobbyID!.uuid,
value.passphrase,
);
context.push('/game', extra: chessGameArgs);
});
}
Future<PlayerInfo?> joinPrivateGame() async {
String addr;
Response response;
// server expects us to send the passphrase
var info = PlayerInfo(
playerID: null, lobbyID: null, passphrase: myController.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),
body: jsonEncode(info), headers: {"Accept": "application/json"});
} catch (e) {
log(e.toString());
if (!context.mounted) return null;
const snackBar = SnackBar(
backgroundColor: Colors.amberAccent,
content: Text("mChess server is not responding. Try again or give up"),
);
Future.delayed(const Duration(seconds: 2), () {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(snackBar);
context.push('/'); // We go back to lobby selector
});
return null;
}
if (response.statusCode == 200) {
var info = PlayerInfo.fromJson(jsonDecode(response.body));
log('Player info received from server: ');
log('lobbyID: ${info.lobbyID}');
log('playerID: ${info.playerID}');
log('passphrase: ${info.passphrase}');
return info;
}
return null;
}
}

View File

@ -0,0 +1,146 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:mchess/api/game_info.dart';
import 'package:mchess/connection_cubit/connection_cubit.dart';
import 'package:mchess/pages/chess_game.dart';
import 'package:mchess/utils/config.dart' as config;
import 'package:universal_platform/universal_platform.dart';
class JoinGameHandleWidget extends StatefulWidget {
final String passphrase;
const JoinGameHandleWidget({required this.passphrase, super.key});
@override
State<JoinGameHandleWidget> createState() => _JoinGameHandleWidgetState();
}
class _JoinGameHandleWidgetState extends State<JoinGameHandleWidget> {
late Future<GameInfo?> joinGameFuture;
@override
void initState() {
super.initState();
joinGameFuture = joinPrivateGame(widget.passphrase);
joinGameFuture.then((val) {
if (val == null) return;
ConnectionCubit.getInstance().connectIfNotConnected(
val.playerID!.uuid,
val.passphrase,
);
});
}
@override
Widget build(BuildContext context) {
FloatingActionButton? fltnBtn;
if (UniversalPlatform.isLinux ||
UniversalPlatform.isMacOS ||
UniversalPlatform.isWindows) {
fltnBtn = FloatingActionButton(
child: const Icon(Icons.cancel),
onPressed: () {
context.pop();
},
);
}
var loadingIndicator = const SizedBox(
height: 100,
width: 100,
child: CircularProgressIndicator(),
);
return Scaffold(
floatingActionButton: fltnBtn,
body: Center(
child: FutureBuilder(
future: joinGameFuture,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return loadingIndicator;
} else {
return BlocBuilder<ConnectionCubit, ConnectionCubitState>(
builder: (context, state) {
if (state.iAmConnected) {
return const ChessGame();
} else {
return loadingIndicator;
}
});
}
}),
),
);
}
Future<GameInfo?> joinPrivateGame(String phrase) async {
http.Response response;
var existingInfo = await GameInfo.get(phrase);
log('playerID: ${existingInfo?.playerID} and passphrase: "${existingInfo?.passphrase}"');
GameInfo info;
if (existingInfo?.passphrase == phrase) {
// We have player info for this exact passphrase
info = GameInfo(playerID: existingInfo?.playerID, passphrase: phrase);
} else {
info = GameInfo(playerID: null, passphrase: phrase);
}
try {
response = await http.post(Uri.parse(config.getJoinGameURL()),
body: jsonEncode(info), headers: {"Accept": "application/json"});
} catch (e) {
log(e.toString());
if (!context.mounted) return null;
const snackBar = SnackBar(
backgroundColor: Colors.amberAccent,
content: Text("mChess server is not responding. Try again or give up"),
);
Future.delayed(const Duration(seconds: 1), () {
if (!mounted) return null;
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(snackBar);
context.goNamed('lobbySelector'); // We go back to the lobby selector
});
return null;
}
if (response.statusCode == HttpStatus.notFound) {
const snackBar = SnackBar(
backgroundColor: Colors.amberAccent,
content: Text("Passphrase could not be found."),
);
if (!mounted) return null;
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(snackBar);
context.goNamed('lobbySelector');
return null;
}
if (response.statusCode == HttpStatus.ok) {
var info = GameInfo.fromJson(jsonDecode(response.body));
info.store();
log('Player info received from server: ');
log('playerID: ${info.playerID}');
log('passphrase: ${info.passphrase}');
return info;
}
return null;
}
}

View File

@ -1,10 +1,16 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mchess/utils/passphrase.dart';
class LobbySelector extends StatelessWidget {
class LobbySelector extends StatefulWidget {
const LobbySelector({super.key});
final buttonStyle = const ButtonStyle();
@override
State<LobbySelector> createState() => _LobbySelectorState();
}
class _LobbySelectorState extends State<LobbySelector> {
final phraseController = TextEditingController();
@override
Widget build(BuildContext context) {
@ -15,7 +21,7 @@ class LobbySelector extends StatelessWidget {
children: [
ElevatedButton(
onPressed: () {
_dialogBuilder(context);
context.goNamed('createGame');
},
child: const Row(
mainAxisSize: MainAxisSize.min,
@ -24,7 +30,23 @@ class LobbySelector extends StatelessWidget {
SizedBox(
width: 10,
),
Text('Private game')
Text('Create private game')
],
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
buildEnterPassphraseDialog(context);
},
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.mail),
SizedBox(
width: 10,
),
Text('Join private game')
],
),
),
@ -34,28 +56,45 @@ class LobbySelector extends StatelessWidget {
);
}
Future<void> _dialogBuilder(BuildContext context) {
Future<void> buildEnterPassphraseDialog(BuildContext context) {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Host or join?'),
actions: <Widget>[
TextButton(
child: const Text('Host'),
onPressed: () {
context.push('/host');
},
),
TextButton(
child: const Text('Join'),
onPressed: () {
context.push('/join');
},
),
],
return ScaffoldMessenger(
child: Builder(builder: (builderContext) {
return Scaffold(
backgroundColor: Colors.transparent,
body: AlertDialog(
title: const Text('Enter the passphrase here:'),
content: TextField(
controller: phraseController,
onSubmitted: (val) => submitAction(val),
decoration: InputDecoration(
hintText: 'Enter passphrase here',
suffixIcon: IconButton(
onPressed: () => submitAction(phraseController.text),
icon: const Icon(Icons.check),
)),
),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
builderContext.pop();
},
),
],
),
);
}),
);
},
);
}
void submitAction(String phrase) {
context.pop();
context.goNamed('game', pathParameters: {'phrase': phrase.toURL()});
phraseController.clear();
}
}

View File

@ -1,91 +0,0 @@
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';
import 'package:mchess/api/register.dart';
import 'package:mchess/pages/chess_game.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class PrepareRandomGameWidget extends StatefulWidget {
const PrepareRandomGameWidget({super.key});
@override
State<PrepareRandomGameWidget> createState() =>
_PrepareRandomGameWidgetState();
}
class _PrepareRandomGameWidgetState extends State<PrepareRandomGameWidget> {
late Future randomGameResponse;
@override
void initState() {
randomGameResponse = registerForRandomGame();
goToGameWhenResponseIsHere(randomGameResponse as Future<PlayerInfo?>);
super.initState();
}
void goToGameWhenResponseIsHere(Future<PlayerInfo?> resp) {
resp.then((value) {
if (value == null) return;
context.push(
'/game',
extra: ChessGameArguments(
lobbyID: value.lobbyID!,
playerID: value.playerID!,
passphrase: value.passphrase),
);
});
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: SizedBox(
height: 100,
width: 100,
child: CircularProgressIndicator(),
),
),
);
}
Future<PlayerInfo?> registerForRandomGame() async {
String addr;
Response response;
if (kDebugMode) {
addr = 'http://localhost:8080/api/random';
} else {
addr = 'https://chess.sw-gross.de:9999/api/random';
}
try {
response = await http
.get(Uri.parse(addr), headers: {"Accept": "application/json"});
} catch (e) {
if (!context.mounted) return null;
const snackBar = SnackBar(
backgroundColor: Colors.amberAccent,
content: Text("mChess server is not responding. Try again or give up"),
);
Future.delayed(const Duration(seconds: 2), () {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(snackBar);
context.pop();
});
return null;
}
if (response.statusCode == 200) {
log(response.body);
return PlayerInfo.fromJson(jsonDecode(response.body));
}
return null;
}
}

View File

@ -1,9 +1,13 @@
import 'dart:developer';
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:mchess/pages/chess_game.dart';
import 'package:mchess/pages/join_game.dart';
import 'package:mchess/pages/join_game_handle_widget.dart';
import 'package:mchess/pages/lobby_selector.dart';
import 'package:mchess/pages/host_game.dart';
import 'package:mchess/pages/prepare_random_game.dart';
import 'package:mchess/pages/create_game_widget.dart';
import 'package:mchess/utils/passphrase.dart';
final navigatorKey = GlobalKey<NavigatorState>();
class ChessAppRouter {
static final ChessAppRouter _instance = ChessAppRouter._internal();
@ -15,43 +19,38 @@ class ChessAppRouter {
}
final router = GoRouter(
navigatorKey: navigatorKey,
debugLogDiagnostics: true,
routes: [
GoRoute(
path: '/',
name: 'lobbySelector',
builder: (context, state) => const LobbySelector(),
),
GoRoute(
path: '/prepareRandom',
name: 'prepareRandom',
builder: (context, state) {
return const PrepareRandomGameWidget();
}),
GoRoute(
path: '/host',
name: 'host',
builder: (context, state) {
return const HostGameWidget();
}),
GoRoute(
path: '/join',
name: 'join',
builder: (context, state) {
return const JoinGameWidget();
}),
GoRoute(
path: '/game',
name: 'game',
builder: (context, state) {
var args = state.extra as ChessGameArguments;
return ChessGame(
lobbyID: args.lobbyID,
playerID: args.playerID,
passphrase: args.passphrase,
);
return const LobbySelector();
},
)
routes: [
GoRoute(
path: 'createGame',
name: 'createGame',
builder: (context, state) {
return const CreateGameWidget();
}),
GoRoute(
path: 'game/:phrase',
name: 'game',
builder: (context, state) {
var urlPhrase = state.pathParameters['phrase'];
if (urlPhrase == null) {
log('in /game route builder: url phrase null');
return const LobbySelector();
}
return JoinGameHandleWidget(
passphrase: urlPhrase.toPhraseWithSpaces());
},
)
],
),
],
);
}

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

34
lib/utils/config.dart Normal file
View File

@ -0,0 +1,34 @@
const prodURL = 'chess.sw-gross.de:9999';
const debugURL = 'localhost:8080';
const useDbgUrl = false;
String getCreateGameURL() {
var prot = 'https';
var domain = prodURL;
if (useDbgUrl) {
prot = 'http';
domain = debugURL;
}
return '$prot://$domain/api/hostPrivate';
}
String getJoinGameURL() {
var prot = 'https';
var domain = prodURL;
if (useDbgUrl) {
prot = 'http';
domain = debugURL;
}
return '$prot://$domain/api/joinPrivate';
}
String getWebsocketURL() {
var prot = 'wss';
var domain = prodURL;
if (useDbgUrl) {
prot = 'ws';
domain = debugURL;
}
return '$prot://$domain/api/ws';
}

30
lib/utils/passphrase.dart Normal file
View File

@ -0,0 +1,30 @@
extension PassphaseURL on String {
String capitalize() {
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
}
String toURL() {
var words = split(' ');
for (var i = 0; i < words.length; i++) {
words[i] = words[i].capitalize();
}
return words.join();
}
String toPhraseWithSpaces() {
var phrase = '';
for (var i = 0; i < length; i++) {
if (this[i] == this[i].toUpperCase()) {
phrase += ' ';
}
phrase += this[i].toLowerCase();
}
phrase = phrase.trim();
return phrase.toLowerCase();
}
}

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

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:mchess/chess_bloc/promotion_bloc.dart';
import 'package:mchess/utils/chess_utils.dart';
@ -26,35 +27,31 @@ class PromotionDialog extends StatelessWidget {
children: [
IconButton(
onPressed: () {
Navigator.pop(context);
pieceChosen(ChessPieceClass.queen);
pieceChosen(context, ChessPieceClass.queen);
},
icon: SvgPicture.asset(chessPiecesAssets[ChessPieceAssetKey(
pieceClass: ChessPieceClass.queen, color: sideColor)]!),
iconSize: 200,
iconSize: iconSize,
),
IconButton(
onPressed: () {
Navigator.pop(context);
pieceChosen(ChessPieceClass.rook);
pieceChosen(context, ChessPieceClass.rook);
},
icon: SvgPicture.asset(chessPiecesAssets[ChessPieceAssetKey(
pieceClass: ChessPieceClass.rook, color: sideColor)]!),
iconSize: 100,
iconSize: iconSize,
),
IconButton(
onPressed: () {
Navigator.pop(context);
pieceChosen(ChessPieceClass.knight);
pieceChosen(context, ChessPieceClass.knight);
},
icon: SvgPicture.asset(chessPiecesAssets[ChessPieceAssetKey(
pieceClass: ChessPieceClass.knight, color: sideColor)]!),
iconSize: 10,
iconSize: iconSize,
),
IconButton(
onPressed: () {
Navigator.pop(context);
pieceChosen(ChessPieceClass.bishop);
pieceChosen(context, ChessPieceClass.bishop);
},
icon: SvgPicture.asset(chessPiecesAssets[ChessPieceAssetKey(
pieceClass: ChessPieceClass.bishop, color: sideColor)]!),
@ -65,7 +62,8 @@ class PromotionDialog extends StatelessWidget {
);
}
void pieceChosen(ChessPieceClass pieceClass) {
void pieceChosen(BuildContext context, ChessPieceClass pieceClass) {
context.pop();
PromotionBloc.getInstance()
.add(PieceChosen(pieceClass: pieceClass, color: sideColor));
}

View File

@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

View File

@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: args
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.5.0"
async:
dependency: transitive
description:
@ -21,10 +21,10 @@ packages:
dependency: transitive
description:
name: bloc
sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49"
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.2"
version: "8.1.4"
boolean_selector:
dependency: transitive
description:
@ -69,10 +69,10 @@ packages:
dependency: "direct main"
description:
name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.6"
version: "1.0.8"
fake_async:
dependency: transitive
description:
@ -81,6 +81,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
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
@ -90,26 +114,26 @@ packages:
dependency: "direct main"
description:
name: flutter_bloc
sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
url: "https://pub.dev"
source: hosted
version: "8.1.3"
version: "8.1.6"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
version: "4.0.0"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338"
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
version: "2.0.10+1"
flutter_test:
dependency: "direct dev"
description: flutter
@ -124,18 +148,18 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "5668e6d3dbcb2d0dfa25f7567554b88c57e1e3f3c440b672b24d4a9477017d5b"
sha256: cdae1b9c8bd7efadcef6112e81c903662ef2ce105cbd220a04bbb7c3425b5554
url: "https://pub.dev"
source: hosted
version: "10.1.2"
version: "14.2.0"
http:
dependency: "direct main"
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.2.1"
http_parser:
dependency: transitive
description:
@ -144,14 +168,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "4.0.0"
logging:
dependency: transitive
description:
@ -164,26 +212,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.14.0"
nested:
dependency: transitive
description:
@ -196,10 +244,10 @@ packages:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
version: "1.9.0"
path_parsing:
dependency: transitive
description:
@ -208,22 +256,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.1"
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: transitive
description:
name: provider
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.0.5"
version: "6.1.2"
quiver:
dependency: "direct main"
description:
@ -232,6 +320,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.1"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
sky_engine:
dependency: transitive
description: flutter
@ -289,10 +433,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794"
url: "https://pub.dev"
source: hosted
version: "0.6.1"
version: "0.7.1"
typed_data:
dependency: transitive
description:
@ -301,38 +445,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
universal_platform:
dependency: "direct main"
description:
name: universal_platform
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: e03928880bdbcbf496fb415573f5ab7b1ea99b9b04f669c01104d085893c3134
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "4.4.0"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f"
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
url: "https://pub.dev"
source: hosted
version: "1.1.7"
version: "1.1.11+1"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f"
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
url: "https://pub.dev"
source: hosted
version: "1.1.7"
version: "1.1.11+1"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e"
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
url: "https://pub.dev"
source: hosted
version: "1.1.7"
version: "1.1.11+1"
vector_math:
dependency: transitive
description:
@ -341,30 +493,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b"
url: "https://pub.dev"
source: hosted
version: "14.2.2"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
version: "0.5.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
url: "https://pub.dev"
source: hosted
version: "0.1.5"
web_socket_channel:
dependency: "direct main"
description:
name: web_socket_channel
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "3.0.0"
win32:
dependency: transitive
description:
name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev"
source: hosted
version: "5.5.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
xml:
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.1.0 <4.0.0"
flutter: ">=3.7.0"
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"

View File

@ -40,10 +40,12 @@ dependencies:
cupertino_icons: ^1.0.2
flutter_bloc: ^8.1.3
quiver: ^3.1.0
web_socket_channel: ^2.2.0
go_router: ^10.1.0
web_socket_channel: ^3.0.0
go_router: ^14.0.2
http: ^1.0.0
uuid: ^4.0.0
shared_preferences: ^2.2.2
universal_platform: ^1.0.0+1
dev_dependencies:
flutter_test:
@ -54,7 +56,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
flutter_lints: ^4.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@ -8,16 +8,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mchess/pages/chess_game.dart';
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"),
passphrase: 'test',
));
await tester.pumpWidget(const ChessGame());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);