diff --git a/android/gradle.properties b/android/gradle.properties index bc2d95e8567abcfd41c26ebeb95fced48f43e773..818e87b23b224ced309ae5c147e5ed827826e237 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 0000000000000000000000000000000000000000..c4767c31cd60be21ad424c3e0b8357a7a045e229 --- /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 0000000000000000000000000000000000000000..f787581174948c885580861f9718adf43a570a3a --- /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 9c748d0c4d7bb8aad974e18a4753d99be6fd4901..6f80a6134baa14c33d69a7561a3eae76e6b5df25 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 33a684d9dc3faec8c2264cf90c480c5f5b8cf456..4889074e48fdc5b62c9f0231b03b80164cad8240 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 786f95c825345317a1b5cf284b534a9105a11aba..df87774498bd92650494ef70cdbb33cafab85353 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 7a89189b91864e569f5ed09217c5204db9eee771..2c89418c505ac019dc54658082ce99d2e62d5bcc 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 a519bbaf328b8ab2dae66b0a04fcea56907d644d..91715a8c4469e6c7599b94338562e2e990b65d54 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 21b062bfa07fceb9fd67043f1439d53924a124b3..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..687b5f0ede88f34258a79b2079b01cd7221e6059 --- /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 0000000000000000000000000000000000000000..fa1f9dc766765f63bf8215b6e5fc770fe04ae32d --- /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 0000000000000000000000000000000000000000..b7a21b79479b515402b1e0b1f42ab5af9fc9f31c --- /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 9db74b2c8ca5160d547d889cfd36b84d5c02a950..0000000000000000000000000000000000000000 --- 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 0e92686f6327f64e7d7159bea93081bb3633b3f1..7d63e14c0b6da81ef06c0a2a39d14a1d7e19ac7a 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"