From 9db7eb18129adc2ffdcf71215483ebd5ba0a8b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr> Date: Wed, 28 Aug 2024 14:31:05 +0200 Subject: [PATCH] Add minimal playable game --- android/gradle.properties | 4 +- .../metadata/android/en-US/changelogs/2.txt | 1 + .../metadata/android/fr-FR/changelogs/2.txt | 1 + lib/cubit/game_cubit.dart | 84 ++++++++++++++++-- lib/models/game/board.dart | 17 +++- lib/models/game/game.dart | 58 ++++++++++--- lib/ui/layouts/game_layout.dart | 4 - lib/ui/widgets/game/game_board.dart | 87 ++++++++++++++++++- lib/ui/widgets/game/game_bottom.dart | 21 ----- lib/ui/widgets/game/game_house.dart | 45 ++++++++++ lib/ui/widgets/game/game_player.dart | 31 +++++++ lib/ui/widgets/game/game_score.dart | 38 ++++++++ lib/ui/widgets/game/game_top.dart | 21 ----- pubspec.yaml | 2 +- 14 files changed, 340 insertions(+), 74 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/2.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/2.txt delete mode 100644 lib/ui/widgets/game/game_bottom.dart create mode 100644 lib/ui/widgets/game/game_house.dart create mode 100644 lib/ui/widgets/game/game_player.dart create mode 100644 lib/ui/widgets/game/game_score.dart delete mode 100644 lib/ui/widgets/game/game_top.dart diff --git a/android/gradle.properties b/android/gradle.properties index bc2d95e..818e87b 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -app.versionName=0.0.1 -app.versionCode=1 +app.versionName=0.0.2 +app.versionCode=2 diff --git a/fastlane/metadata/android/en-US/changelogs/2.txt b/fastlane/metadata/android/en-US/changelogs/2.txt new file mode 100644 index 0000000..c4767c3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/2.txt @@ -0,0 +1 @@ +Add minimal playable game. diff --git a/fastlane/metadata/android/fr-FR/changelogs/2.txt b/fastlane/metadata/android/fr-FR/changelogs/2.txt new file mode 100644 index 0000000..f787581 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/2.txt @@ -0,0 +1 @@ +Ajout d'un jeu jouable minimal. diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart index 9c748d0..6f80a61 100644 --- a/lib/cubit/game_cubit.dart +++ b/lib/cubit/game_cubit.dart @@ -31,13 +31,13 @@ class GameCubit extends HydratedCubit<GameState> { isStarted: state.currentGame.isStarted, isFinished: state.currentGame.isFinished, animationInProgress: state.currentGame.animationInProgress, - boardAnimated: state.currentGame.boardAnimated, // Base data board: state.currentGame.board, // Game data + currentPlayer: state.currentGame.currentPlayer, scores: state.currentGame.scores, ); - // game.dump(); + game.dump(); updateState(game); } @@ -74,16 +74,84 @@ class GameCubit extends HydratedCubit<GameState> { refresh(); } - // FIXME: should be removed - void doSomething() { - printlog('shoud do something!'); + void toggleCurrentPlayer() { + state.currentGame.currentPlayer = 1 - state.currentGame.currentPlayer; refresh(); } - // FIXME: should be removed - void logSomething([dynamic yolo]) { - printlog('logSomething: $yolo'); + void tapOnCell(int cellIndex) { + printlog('tapOnCell: $cellIndex'); + + if (!state.currentGame.isCurrentPlayerHouse(cellIndex)) { + printlog('not allowed'); + + return; + } + + if (state.currentGame.board.cells[cellIndex] == 0) { + printlog('empty cell'); + + return; + } + + if (!state.currentGame.isMoveAllowed(cellIndex)) { + printlog('not allowed (need to give at least one seed to other player)'); + + return; + } + + state.currentGame.animationInProgress = true; + refresh(); + + final int lastCellIndex = animateSeedsDistribution(cellIndex); + animateSeedsEarning(lastCellIndex); + + toggleCurrentPlayer(); + + state.currentGame.animationInProgress = false; + refresh(); + } + + int animateSeedsDistribution(int sourceCellIndex) { + printlog('animateSeedsDistribution / sourceCellIndex: $sourceCellIndex'); + + final int seedsCount = state.currentGame.board.cells[sourceCellIndex]; + + // empty source cell + state.currentGame.board.cells[sourceCellIndex] = 0; + printlog('animateSeedsDistribution / empty source cell'); refresh(); + + int cellIndex = sourceCellIndex; + for (int i = 0; i < seedsCount; i++) { + cellIndex = state.currentGame.getNextCellIndex(cellIndex, sourceCellIndex); + state.currentGame.board.cells[cellIndex] += 1; + refresh(); + } + + refresh(); + + return cellIndex; + } + + void animateSeedsEarning(int lastCellIndex) { + printlog('animateSeedsEarning / lastCellIndex: $lastCellIndex'); + + if (state.currentGame.isOpponentHouse(lastCellIndex)) { + final int seedsCount = state.currentGame.board.cells[lastCellIndex]; + printlog('found $seedsCount seed(s) on final house.'); + + if ([2, 3].contains(seedsCount)) { + printlog('ok will earn these seeds.'); + + state.currentGame.board.cells[lastCellIndex] = 0; + state.currentGame.scores[state.currentGame.currentPlayer] += seedsCount; + refresh(); + + // (recursively) check previous cells + animateSeedsEarning(state.currentGame.getPreviousCellIndex(lastCellIndex)); + } + } } @override diff --git a/lib/models/game/board.dart b/lib/models/game/board.dart index 33a684d..4889074 100644 --- a/lib/models/game/board.dart +++ b/lib/models/game/board.dart @@ -25,8 +25,21 @@ class Board { void dump() { printlog(''); - printlog('$Board:'); - printlog(' cells: $cells'); + printlog(' $Board:'); + + const List<List<int>> indexes = [ + [11, 10, 9, 8, 7, 6], + [0, 1, 2, 3, 4, 5], + ]; + + for (List<int> line in indexes) { + String row = ' '; + for (int index in line) { + row += '[${cells[index].toString().padLeft(2, ' ')}]'; + } + printlog(row); + } + printlog(''); } diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart index 786f95c..df87774 100644 --- a/lib/models/game/game.dart +++ b/lib/models/game/game.dart @@ -3,14 +3,6 @@ import 'package:awale/models/settings/settings_game.dart'; import 'package:awale/models/settings/settings_global.dart'; import 'package:awale/utils/tools.dart'; -typedef MovingTile = String; -typedef Move = Board; -typedef Player = String; -typedef ConflictsCount = List<List<int>>; -typedef AnimatedBoard = List<List<bool>>; -typedef AnimatedBoardSequence = List<AnimatedBoard>; -typedef Word = String; - class Game { Game({ // Settings @@ -22,12 +14,12 @@ class Game { this.isStarted = false, this.isFinished = false, this.animationInProgress = false, - this.boardAnimated = const [], // Base data required this.board, // Game data + required this.currentPlayer, required this.scores, }); @@ -40,12 +32,12 @@ class Game { bool isStarted; bool isFinished; bool animationInProgress; - AnimatedBoard boardAnimated; // Base data final Board board; // Game data + int currentPlayer; List<int> scores; factory Game.createNull() { @@ -56,6 +48,7 @@ class Game { // Base data board: Board.createNull(), // Game data + currentPlayer: 0, scores: [0, 0], ); } @@ -80,16 +73,57 @@ class Game { globalSettings: newGlobalSettings, // State isRunning: true, - boardAnimated: [], // Base data board: Board.createNew(cells: cells), // Game data + currentPlayer: 0, scores: [0, 0], ); } bool get canBeResumed => isStarted && !isFinished; + int getNextCellIndex(int cellIndex, int firstCellIndex) { + final int nextCellIndex = (cellIndex + 1) % board.cells.length; + + if (nextCellIndex == firstCellIndex) { + return getNextCellIndex(nextCellIndex, firstCellIndex); + } + + return nextCellIndex; + } + + int getPreviousCellIndex(int cellIndex) { + return (cellIndex - 1) % board.cells.length; + } + + bool isCurrentPlayerHouse(int cellIndex) { + const allowedCellIndexes = [ + [0, 1, 2, 3, 4, 5], + [6, 7, 8, 9, 10, 11], + ]; + return allowedCellIndexes[currentPlayer].contains(cellIndex); + } + + bool isOpponentHouse(int cellIndex) { + return !isCurrentPlayerHouse(cellIndex); + } + + // Ensure move is allowed, from cell seeds count + bool isMoveAllowed(int cellIndex) { + final int seedsCount = board.cells[cellIndex]; + + int finalCellIndex = cellIndex; + for (int i = 0; i < seedsCount; i++) { + finalCellIndex = getNextCellIndex(finalCellIndex, cellIndex); + if (isOpponentHouse(finalCellIndex)) { + return true; + } + } + + return false; + } + void dump() { printlog(''); printlog('## Current game dump:'); @@ -103,7 +137,6 @@ class Game { printlog(' isStarted: $isStarted'); printlog(' isFinished: $isFinished'); printlog(' animationInProgress: $animationInProgress'); - printlog('board:'); board.dump(); printlog(' Game data'); printlog(' scores: $scores'); @@ -125,7 +158,6 @@ class Game { 'isStarted': isStarted, 'isFinished': isFinished, 'animationInProgress': animationInProgress, - 'boardAnimated': boardAnimated, // Base data 'board': board.toJson(), // Game data diff --git a/lib/ui/layouts/game_layout.dart b/lib/ui/layouts/game_layout.dart index 7a89189..2c89418 100644 --- a/lib/ui/layouts/game_layout.dart +++ b/lib/ui/layouts/game_layout.dart @@ -4,9 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awale/cubit/game_cubit.dart'; import 'package:awale/models/game/game.dart'; import 'package:awale/ui/widgets/game/game_board.dart'; -import 'package:awale/ui/widgets/game/game_bottom.dart'; import 'package:awale/ui/widgets/game/game_end.dart'; -import 'package:awale/ui/widgets/game/game_top.dart'; class GameLayout extends StatelessWidget { const GameLayout({super.key}); @@ -24,11 +22,9 @@ class GameLayout extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const GameTopWidget(), const SizedBox(height: 8), const GameBoardWidget(), const SizedBox(height: 8), - const GameBottomWidget(), const Expanded(child: SizedBox.shrink()), currentGame.isFinished ? const GameEndWidget() : const SizedBox.shrink(), ], diff --git a/lib/ui/widgets/game/game_board.dart b/lib/ui/widgets/game/game_board.dart index a519bba..91715a8 100644 --- a/lib/ui/widgets/game/game_board.dart +++ b/lib/ui/widgets/game/game_board.dart @@ -3,6 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:awale/cubit/game_cubit.dart'; import 'package:awale/models/game/game.dart'; +import 'package:awale/ui/widgets/game/game_house.dart'; +import 'package:awale/ui/widgets/game/game_player.dart'; +import 'package:awale/ui/widgets/game/game_score.dart'; class GameBoardWidget extends StatelessWidget { const GameBoardWidget({super.key}); @@ -13,9 +16,89 @@ class GameBoardWidget extends StatelessWidget { child: BlocBuilder<GameCubit, GameState>( builder: (BuildContext context, GameState gameState) { final Game currentGame = gameState.currentGame; + final Color borderColor = Theme.of(context).colorScheme.onSurface; - // FIXME: should be implemented - return Text(currentGame.toString()); + Widget getHouseContent(int cellIndex) { + final bool isTapAllowed = currentGame.isCurrentPlayerHouse(cellIndex); + + return GestureDetector( + onTap: () { + if (isTapAllowed && !currentGame.animationInProgress) { + BlocProvider.of<GameCubit>(context).tapOnCell(cellIndex); + } + }, + child: GameHouseWidget( + seedsCount: currentGame.board.cells[cellIndex], + active: isTapAllowed, + ), + ); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GamePlayerWidget( + active: currentGame.currentPlayer == 0, + ), + Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: borderColor, + borderRadius: BorderRadius.circular(2), + border: Border.all( + color: borderColor, + width: 2, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GameScoreWidget( + score: currentGame.scores[0], + ), + Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + TableRow(children: [ + getHouseContent(0), + getHouseContent(11), + ]), + TableRow(children: [ + getHouseContent(1), + getHouseContent(10), + ]), + TableRow(children: [ + getHouseContent(2), + getHouseContent(9), + ]), + TableRow(children: [ + getHouseContent(3), + getHouseContent(8), + ]), + TableRow(children: [ + getHouseContent(4), + getHouseContent(7), + ]), + TableRow(children: [ + getHouseContent(5), + getHouseContent(6), + ]), + ], + ), + GameScoreWidget( + score: currentGame.scores[1], + ) + ], + ), + ), + GamePlayerWidget( + active: currentGame.currentPlayer == 1, + ), + ], + ); }, ), ); diff --git a/lib/ui/widgets/game/game_bottom.dart b/lib/ui/widgets/game/game_bottom.dart deleted file mode 100644 index 21b062b..0000000 --- a/lib/ui/widgets/game/game_bottom.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:awale/cubit/game_cubit.dart'; -import 'package:awale/models/game/game.dart'; - -class GameBottomWidget extends StatelessWidget { - const GameBottomWidget({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder<GameCubit, GameState>( - builder: (BuildContext context, GameState gameState) { - final Game currentGame = gameState.currentGame; - - // FIXME: should be implemented - return Text(currentGame.board.toString()); - }, - ); - } -} diff --git a/lib/ui/widgets/game/game_house.dart b/lib/ui/widgets/game/game_house.dart new file mode 100644 index 0000000..687b5f0 --- /dev/null +++ b/lib/ui/widgets/game/game_house.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class GameHouseWidget extends StatelessWidget { + const GameHouseWidget({ + super.key, + required this.seedsCount, + required this.active, + }); + + final int seedsCount; + final bool active; + + @override + Widget build(BuildContext context) { + final Color borderColor = active + ? Theme.of(context).colorScheme.onSecondary + : Theme.of(context).colorScheme.onTertiary; + + return AspectRatio( + aspectRatio: 1, + child: Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: borderColor, + borderRadius: BorderRadius.circular(2), + border: Border.all( + color: borderColor, + width: 2, + ), + ), + width: 50, + child: Text( + seedsCount.toString(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + } +} diff --git a/lib/ui/widgets/game/game_player.dart b/lib/ui/widgets/game/game_player.dart new file mode 100644 index 0000000..fa1f9dc --- /dev/null +++ b/lib/ui/widgets/game/game_player.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class GamePlayerWidget extends StatelessWidget { + const GamePlayerWidget({ + super.key, + required this.active, + }); + + final bool active; + + @override + Widget build(BuildContext context) { + final Color baseColor = active ? Colors.pink : Theme.of(context).colorScheme.surface; + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: baseColor, + borderRadius: BorderRadius.circular(2), + border: Border.all( + color: baseColor, + width: 2, + ), + ), + width: 100, + height: 100, + child: const SizedBox.shrink(), + ); + } +} diff --git a/lib/ui/widgets/game/game_score.dart b/lib/ui/widgets/game/game_score.dart new file mode 100644 index 0000000..b7a21b7 --- /dev/null +++ b/lib/ui/widgets/game/game_score.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class GameScoreWidget extends StatelessWidget { + const GameScoreWidget({ + super.key, + required this.score, + }); + + final int score; + + @override + Widget build(BuildContext context) { + final Color borderColor = Theme.of(context).colorScheme.onPrimary; + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: borderColor, + borderRadius: BorderRadius.circular(2), + border: Border.all( + color: borderColor, + width: 2, + ), + ), + width: 100, + child: Text( + score.toString(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 50, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + ); + } +} diff --git a/lib/ui/widgets/game/game_top.dart b/lib/ui/widgets/game/game_top.dart deleted file mode 100644 index 9db74b2..0000000 --- a/lib/ui/widgets/game/game_top.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:awale/cubit/game_cubit.dart'; -import 'package:awale/models/game/game.dart'; - -class GameTopWidget extends StatelessWidget { - const GameTopWidget({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder<GameCubit, GameState>( - builder: (BuildContext context, GameState gameState) { - final Game currentGame = gameState.currentGame; - - // FIXME: should be implemented - return Text(currentGame.scores.toString()); - }, - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 0e92686..7d63e14 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Awale game publish_to: "none" -version: 0.0.1+1 +version: 0.0.2+2 environment: sdk: "^3.0.0" -- GitLab