From 446eb261cb691801b6b87a39297e2e32122c730e Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Wed, 27 Mar 2024 00:38:42 +0500 Subject: [PATCH] Remove roles seed, reimplement choose roles algorithm --- lib/game/player.dart | 6 +- lib/screens/choose_roles_screen.dart | 90 ++++++++++++++++++----- lib/screens/debug_menu_screen.dart | 103 +++++++++------------------ lib/screens/roles.dart | 76 ++++++++------------ lib/screens/settings/main.dart | 5 +- lib/utils/bug_report/stub.dart | 5 -- lib/utils/extensions.dart | 5 +- lib/utils/find_seed.dart | 27 ------- lib/utils/game_controller.dart | 32 +++++---- lib/widgets/game_state.dart | 4 -- 10 files changed, 164 insertions(+), 189 deletions(-) delete mode 100644 lib/utils/find_seed.dart diff --git a/lib/game/player.dart b/lib/game/player.dart index 3aaa833..87d43fd 100644 --- a/lib/game/player.dart +++ b/lib/game/player.dart @@ -1,5 +1,3 @@ -import "dart:math"; - import "package:meta/meta.dart"; import "../utils/extensions.dart"; @@ -88,9 +86,9 @@ class Player { List generatePlayers({ List? nicknames, - Random? random, + List? roles, }) { - final playerRoles = List.of(rolesList)..shuffle(random); + final playerRoles = roles ?? (List.of(rolesList)..shuffle()); return [ for (var i = 0; i < playerRoles.length; i++) Player( diff --git a/lib/screens/choose_roles_screen.dart b/lib/screens/choose_roles_screen.dart index 3110c82..eed11c7 100644 --- a/lib/screens/choose_roles_screen.dart +++ b/lib/screens/choose_roles_screen.dart @@ -1,6 +1,5 @@ import "dart:async"; -import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:provider/provider.dart"; @@ -8,7 +7,6 @@ import "../game/player.dart"; import "../utils/db/repo.dart"; import "../utils/errors.dart"; import "../utils/extensions.dart"; -import "../utils/find_seed.dart"; import "../utils/game_controller.dart"; import "../utils/ui.dart"; import "../widgets/confirmation_dialog.dart"; @@ -32,7 +30,6 @@ class _ChooseRolesScreenState extends State { (_) => PlayerRole.values.toSet(), growable: false, ); - var _isInProgress = false; final _errorsByRole = {}; final _errorsByIndex = {}; final _chosenNicknames = List.generate(rolesList.length, (index) => null); @@ -110,6 +107,74 @@ class _ChooseRolesScreenState extends State { ..addAll(byIndex); } + List? _randomizeRoles() { + final results = >[]; + final count = rolesList.length; + for (var iDon = 0; iDon < count; iDon++) { + if (!_roles[iDon].contains(PlayerRole.don)) { + continue; + } + for (var iSheriff = 0; iSheriff < count; iSheriff++) { + if (!_roles[iSheriff].contains(PlayerRole.sheriff) || iSheriff == iDon) { + continue; + } + for (var iMafia = 0; iMafia < count; iMafia++) { + if (!_roles[iMafia].contains(PlayerRole.mafia) || iMafia == iDon || iMafia == iSheriff) { + continue; + } + for (var jMafia = iMafia + 1; jMafia < count; jMafia++) { + if (!_roles[jMafia].contains(PlayerRole.mafia) || + jMafia == iDon || + jMafia == iSheriff) { + continue; + } + var valid = true; + for (var iCitizen = 0; iCitizen < count; iCitizen++) { + if (iCitizen == iDon || + iCitizen == iSheriff || + iCitizen == iMafia || + iCitizen == jMafia) { + continue; + } + if (!_roles[iCitizen].contains(PlayerRole.citizen)) { + valid = false; + break; + } + } + if (valid) { + results.add([ + for (var i = 0; i < count; i++) + i == iDon + ? PlayerRole.don + : i == iSheriff + ? PlayerRole.sheriff + : i == iMafia || i == jMafia + ? PlayerRole.mafia + : PlayerRole.citizen, + ]); + } + } + } + } + } + if (results.isEmpty) { + return null; + } + final result = results.randomElement; + assert( + () { + for (var i = 0; i < rolesList.length; i++) { + if (!_roles[i].contains(result[i])) { + return false; + } + } + return true; + }(), + "Roles are invalid", + ); + return result; + } + Future _onFabPressed(BuildContext context) async { setState(_validate); if (_errorsByIndex.isNotEmpty || _errorsByRole.isNotEmpty) { @@ -117,14 +182,8 @@ class _ChooseRolesScreenState extends State { return; } final controller = context.read(); - final initialSeed = controller.rolesSeed ?? getNewSeed(); - setState(() => _isInProgress = true); - final newSeed = await compute(findSeedIsolateWrapper, (initialSeed, _roles)); - setState(() => _isInProgress = false); - if (!context.mounted) { - throw ContextNotMountedError(); - } - if (newSeed == null) { + final newRoles = _randomizeRoles(); + if (newRoles == null) { showSnackBar( context, const SnackBar(content: Text("Невозможно применить выбранные роли")), @@ -132,8 +191,9 @@ class _ChooseRolesScreenState extends State { return; } controller - ..rolesSeed = newSeed - ..nicknames = _chosenNicknames; + ..roles = newRoles + ..nicknames = _chosenNicknames + ..startNewGame(); final showRoles = await showDialog( context: context, builder: (context) => const ConfirmationDialog( @@ -273,9 +333,7 @@ class _ChooseRolesScreenState extends State { floatingActionButton: FloatingActionButton( tooltip: "Применить", onPressed: () => _onFabPressed(context), - child: _isInProgress - ? const SizedBox.square(dimension: 24, child: CircularProgressIndicator()) - : const Icon(Icons.check), + child: const Icon(Icons.check), ), ); } diff --git a/lib/screens/debug_menu_screen.dart b/lib/screens/debug_menu_screen.dart index 4b7e2ff..113b14b 100644 --- a/lib/screens/debug_menu_screen.dart +++ b/lib/screens/debug_menu_screen.dart @@ -1,85 +1,46 @@ import "package:flutter/material.dart"; -import "package:provider/provider.dart"; -import "../utils/errors.dart"; -import "../utils/game_controller.dart"; import "../utils/ui.dart"; -import "../widgets/confirmation_dialog.dart"; import "../widgets/list_tiles/text_field.dart"; class DebugMenuScreen extends StatelessWidget { const DebugMenuScreen({super.key}); @override - Widget build(BuildContext context) { - final controller = context.watch(); - return Scaffold( - appBar: AppBar(title: const Text("Меню отладки")), - body: ListView( - children: [ - TextFieldListTile( - leading: const Icon(Icons.casino), - title: const Text("Зерно генератора ролей"), - subtitle: const Text("При изменении игра будет перезапущена"), - keyboardType: TextInputType.number, - initialText: controller.rolesSeed?.toString() ?? "", - labelText: "Зерно генератора ролей", - onSubmit: (value) async { - final seed = int.tryParse(value); - if (seed != null) { - final res = await showDialog( - context: context, - builder: (context) => const ConfirmationDialog( - title: Text("Применить настройки?"), - content: Text("Текущая игра будет перезапущена. Продолжить?"), - ), - ); - if (res ?? false) { - controller - ..rolesSeed = seed - ..stopGame(); + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text("Меню отладки")), + body: ListView( + children: [ + TextFieldListTile( + leading: const Icon(Icons.arrow_forward), + title: const Text("Перейти к экрану"), + initialText: "/", + labelText: "Путь", + validator: (value) { + if (value == null || value.isEmpty) { + return "Введите путь"; + } + if (!value.startsWith("/")) { + return "Путь должен начинаться с /"; + } + return null; + }, + onSubmit: (value) async { + try { + await Navigator.pushNamed(context, value); + } catch (e) { if (!context.mounted) { - throw ContextNotMountedError(); + return; } - showSnackBar( - context, - const SnackBar(content: Text("Игра перезапущена")), + await showSimpleDialog( + context: context, + title: const Text("Ошибка"), + content: Text(e.toString()), ); } - } - }, - ), - TextFieldListTile( - leading: const Icon(Icons.arrow_forward), - title: const Text("Перейти к экрану"), - initialText: "/", - labelText: "Путь", - validator: (value) { - if (value == null || value.isEmpty) { - return "Введите путь"; - } - if (!value.startsWith("/")) { - return "Путь должен начинаться с /"; - } - return null; - }, - onSubmit: (value) async { - try { - await Navigator.pushNamed(context, value); - } catch (e) { - if (!context.mounted) { - return; - } - await showSimpleDialog( - context: context, - title: const Text("Ошибка"), - content: Text(e.toString()), - ); - } - }, - ), - ], - ), - ); - } + }, + ), + ], + ), + ); } diff --git a/lib/screens/roles.dart b/lib/screens/roles.dart index 87c7c39..ad074d0 100644 --- a/lib/screens/roles.dart +++ b/lib/screens/roles.dart @@ -1,64 +1,50 @@ -import "dart:math"; - import "package:flutter/material.dart"; import "package:provider/provider.dart"; -import "../game/player.dart"; import "../utils/game_controller.dart"; -import "../utils/log.dart"; import "../utils/ui.dart"; class RolesScreen extends StatelessWidget { - static final _log = Logger("RolesScreen"); - const RolesScreen({super.key}); @override Widget build(BuildContext context) { final controller = context.watch(); - final players = controller.isGameInitialized - ? controller.players - : controller.rolesSeed != null - ? generatePlayers(random: Random(controller.rolesSeed), nicknames: controller.nicknames) - : const []; - if (players.isEmpty) { - _log.warning("Players is empty"); - } + final players = controller.players; + assert(players.isNotEmpty, "Players must be non-empty. Is the game running?"); return Scaffold( appBar: AppBar( title: const Text("Раздача ролей"), ), - body: players.isNotEmpty - ? PageView.builder( - itemCount: players.length, - itemBuilder: (context, index) { - final player = players[index]; - final String topText; - if (player.nickname != null) { - topText = "${player.nickname} (Игрок #${player.number})"; - } else { - topText = "Игрок #${player.number}"; - } - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - topText, - style: const TextStyle(fontSize: 20), - textAlign: TextAlign.center, - ), - Text( - player.role.prettyName, - style: const TextStyle(fontSize: 48), - textAlign: TextAlign.center, - ), - ], - ), - ); - }, - ) - : const Center(child: Text(r"¯\_(ツ)_/¯", style: TextStyle(fontSize: 48))), + body: PageView.builder( + itemCount: players.length, + itemBuilder: (context, index) { + final player = players[index]; + final String topText; + if (player.nickname != null) { + topText = "${player.nickname} (Игрок #${player.number})"; + } else { + topText = "Игрок #${player.number}"; + } + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + topText, + style: const TextStyle(fontSize: 20), + textAlign: TextAlign.center, + ), + Text( + player.role.prettyName, + style: const TextStyle(fontSize: 48), + textAlign: TextAlign.center, + ), + ], + ), + ); + }, + ), ); } } diff --git a/lib/screens/settings/main.dart b/lib/screens/settings/main.dart index 47d399a..be80fd7 100644 --- a/lib/screens/settings/main.dart +++ b/lib/screens/settings/main.dart @@ -118,9 +118,8 @@ class SettingsScreen extends StatelessWidget { showSimpleDialog( context: context, title: const Text("Сообщить о проблеме"), - content: Text( - "Для сообщения о проблеме нужно поделиться файлом мне в ЛС в Telegram.\n\n" - "Зерно генерации ролей: ${controller.rolesSeed}", + content: const Text( + "Для сообщения о проблеме нужно поделиться файлом мне в ЛС в Telegram.", ), extraActions: [ TextButton( diff --git a/lib/utils/bug_report/stub.dart b/lib/utils/bug_report/stub.dart index ca886ae..d722f76 100644 --- a/lib/utils/bug_report/stub.dart +++ b/lib/utils/bug_report/stub.dart @@ -36,21 +36,17 @@ class BugReportInfo { } class GameInfo { - final int seed; final List log; const GameInfo({ - required this.seed, required this.log, }); Map toJson() => { - "seed": seed, "log": log.map((e) => e.toJson()).toList(), }; factory GameInfo.fromJson(Map json) => GameInfo( - seed: json["seed"] as int, // assuming bug reports are checked in the same app version as the game was played log: (json["log"] as List).parseJsonList( (e) => gameLogFromJson(e, version: GameLogVersion.latest), @@ -70,7 +66,6 @@ Future reportBugCommonImpl(BuildContext context) async { BugReportInfo( packageInfo: packageInfo.data, game: GameInfo( - seed: controller.rolesSeed!, log: controller.gameLog.toUnmodifiableList(), ), ).toJson(), diff --git a/lib/utils/extensions.dart b/lib/utils/extensions.dart index 1e76a55..27c3118 100644 --- a/lib/utils/extensions.dart +++ b/lib/utils/extensions.dart @@ -20,8 +20,9 @@ extension IsAnyOf on T { bool isAnyOf(Iterable values) => values.contains(this); } -extension RandomElement on List { - T get randomElement => this[Random().nextInt(length)]; +extension RandomElement on Iterable { + T get randomElement => + length != 0 ? elementAt(Random().nextInt(length)) : (throw StateError("no element")); } extension RemovePrefix on String { diff --git a/lib/utils/find_seed.dart b/lib/utils/find_seed.dart deleted file mode 100644 index 4a85f77..0000000 --- a/lib/utils/find_seed.dart +++ /dev/null @@ -1,27 +0,0 @@ -import "dart:math"; - -import "../game/player.dart"; -import "extensions.dart"; - -int? findSeed({required int initialSeed, required List> requiredRoles}) { - var newSeed = initialSeed; - var isOk = true; - for (; newSeed < initialSeed + 500000; newSeed++) { - final newRoles = - generatePlayers(random: Random(newSeed)).map((p) => p.role).toUnmodifiableList(); - isOk = true; - for (var i = 0; i < 10; i++) { - if (!requiredRoles[i].contains(newRoles[i])) { - isOk = false; - break; - } - } - if (isOk) { - break; - } - } - return isOk ? newSeed : null; -} - -int? findSeedIsolateWrapper((int initialSeed, List> requiredRoles) data) => - findSeed(initialSeed: data.$1, requiredRoles: data.$2); diff --git a/lib/utils/game_controller.dart b/lib/utils/game_controller.dart index 8b12935..1fe69d8 100644 --- a/lib/utils/game_controller.dart +++ b/lib/utils/game_controller.dart @@ -1,5 +1,3 @@ -import "dart:math"; - import "package:flutter/material.dart"; import "../game/controller.dart"; @@ -20,8 +18,23 @@ class GameController with ChangeNotifier { Game? _game; - int? rolesSeed; - List? nicknames; + List? _nicknames; + + List? get nicknames => _nicknames; + + set nicknames(List? value) { + assert(value == null || value.length == 10, "Nicknames list must have 10 elements"); + _nicknames = value; + } + + List? _roles; + + List? get roles => _roles; + + set roles(List? value) { + assert(value == null || value.length == 10, "Roles list must have 10 elements"); + _roles = value; + } GameController(); @@ -48,19 +61,14 @@ class GameController with ChangeNotifier { List get players => _game?.players.toUnmodifiableList() ?? const []; void startNewGame() { - if (rolesSeed == null) { - rolesSeed = getNewSeed(); - _log.warning("Roles seed is not set, generating new one: $rolesSeed"); - } - _game = Game.withPlayers(generatePlayers(nicknames: nicknames, random: Random(rolesSeed))); - _log.debug("Game started with seed $rolesSeed"); + _game = Game.withPlayers(generatePlayers(nicknames: _nicknames, roles: _roles)); notifyListeners(); } void stopGame() { _game = null; - rolesSeed = null; - nicknames = null; + _roles = null; + _nicknames = null; _log.debug("Game stopped"); notifyListeners(); } diff --git a/lib/widgets/game_state.dart b/lib/widgets/game_state.dart index 88aee97..786aa22 100644 --- a/lib/widgets/game_state.dart +++ b/lib/widgets/game_state.dart @@ -61,10 +61,6 @@ class BottomGameStateWidget extends StatelessWidget { throw ContextNotMountedError(); } await Navigator.pushNamed(context, "/chooseRoles"); - if (controller.rolesSeed != null) { - // roles were initialized, so we can start the game - controller.startNewGame(); - } } @override