Skip to content

Commit

Permalink
[minesweeper] improved logic for more game modes
Browse files Browse the repository at this point in the history
  • Loading branch information
liplum committed May 6, 2024
1 parent 2815366 commit 8bc4321
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 150 deletions.
2 changes: 2 additions & 0 deletions assets/l10n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,8 @@ game:
timeSpent: "You spent {}"
gameMode:
easy: Easy
normal: Normal
hard: Hard
permission:
permissionDenied: Permission denied
permissionDeniedDescOf: "{} permission was not granted. Please check the app settings."
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/zh-Hans.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,8 @@ game:
timeSpent: "用时 {}"
gameMode:
easy: 简单
normal: 普通
hard: 困难
permission:
permissionDenied: 没有权限
permissionDeniedDescOf: "{}权限未被授权,请检查应用的设置。"
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/zh-Hant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,8 @@ game:
timeSpent: "用時 {}"
gameMode:
easy: 簡單
normal: 普通
hard: 困難
permission:
permissionDenied: 沒有權限
permissionDeniedDescOf: "{}權限未授予許可,請檢查應用程式的設定。"
Expand Down
25 changes: 13 additions & 12 deletions lib/design/adaptive/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,36 +29,36 @@ class Editor {
_customEditor.containsKey(test.runtimeType);
}

static Future<dynamic> showAnyEditor(
static Future<T?> showAnyEditor<T>(
BuildContext context, {
dynamic initial,
required T initial,
String? desc,
bool readonlyIfNotSupport = true,
}) async {
if (initial is int) {
return await showIntEditor(context, desc: desc, initial: initial);
return await showIntEditor(context, desc: desc, initial: initial) as T?;
} else if (initial is String) {
return await showStringEditor(context, desc: desc, initial: initial);
return await showStringEditor(context, desc: desc, initial: initial) as T?;
} else if (initial is bool) {
return await showBoolEditor(context, desc: desc, initial: initial);
return await showBoolEditor(context, desc: desc, initial: initial) as T?;
} else if (initial is DateTime) {
return await showDateTimeEditor(
context,
desc: desc,
initial: initial,
firstDate: DateTime(0),
lastDate: DateTime(9999),
);
) as T?;
} else {
final customEditorBuilder = _customEditor[initial.runtimeType];
if (customEditorBuilder != null) {
return await showAdaptiveDialog(
return await showAdaptiveDialog<T>(
context: context,
builder: (ctx) => customEditorBuilder(ctx, desc, initial),
);
} else {
if (readonlyIfNotSupport) {
return await showReadonlyEditor(context, desc: desc, initial: initial);
return await showReadonlyEditor(context, desc: desc, initial: initial) as T?;
} else {
throw UnsupportedError("Editing $initial is not supported.");
}
Expand Down Expand Up @@ -211,7 +211,7 @@ class _EnumEditorState<T> extends State<EnumEditor<T>> {
make: (ctx) => PlatformTextButton(
child: current.toString().text(),
onPressed: () async {
FixedExtentScrollController controller = FixedExtentScrollController(initialItem: initialIndex);
final controller = FixedExtentScrollController(initialItem: initialIndex);
controller.addListener(() {
final selected = widget.values[controller.selectedItem];
if (selected != current) {
Expand All @@ -221,9 +221,10 @@ class _EnumEditorState<T> extends State<EnumEditor<T>> {
}
});
await ctx.showPicker(
count: widget.values.length,
controller: controller,
make: (ctx, index) => widget.values[index].toString().text());
count: widget.values.length,
controller: controller,
make: (ctx, index) => widget.values[index].toString().text(),
);
controller.dispose();
},
),
Expand Down
37 changes: 23 additions & 14 deletions lib/game/minesweeper/entity/board.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Board {
late List<Cell> _cells;

static const _nearbyDelta = [(-1, 1), (0, 1), (1, 1), (-1, 0), /*(0,0)*/ (1, 0), (-1, -1), (0, -1), (1, -1)];
static const _nearbyDeltaAndThis = [..._nearbyDelta, (0, 0)];

Board({required this.rows, required this.columns}) {
_cells = List.generate(rows * columns, (index) => Cell(row: index ~/ columns, col: index % columns));
Expand Down Expand Up @@ -88,22 +89,30 @@ class Board {
return count;
}

void randomMines({required number, required clickRow, required clickCol}) {
void randomMines({required int number, required int clickRow, required int clickCol}) {
final rand = Random();
final candidates = List.generate(rows * columns, (index) => (row: index ~/ columns, column: index % columns));
// Clicked cell and one-cell nearby cells can't be mines.
for (final (dx, dy) in _nearbyDeltaAndThis) {
final row = clickRow + dx;
final column = clickCol + dy;
candidates.remove((row: row, column: column));
}
final maxMines = candidates.length - 1;
assert(number <= maxMines, "The max mine is $maxMines, but $number is given.");
number = min<int>(number, maxMines);
_mines = number;
int beginSafeRow = clickRow - 1 < 0 ? 0 : clickRow - 1;
int endSafeRow = clickRow + 1 >= rows ? rows - 1 : clickRow + 1;
int beginSafeCol = clickCol - 1 < 0 ? 0 : clickCol - 1;
int endSafeCol = clickCol + 1 >= columns ? columns - 1 : clickCol + 1;
var cnt = 0;
while (cnt < number) {
var value = Random().nextInt(columns * rows);
var col = value % columns;
var row = (value / columns).floor();
final cell = getCell(row: row, col: col);
if (!cell.mine && !((row >= beginSafeRow && row <= endSafeRow) && (col >= beginSafeCol && col <= endSafeCol))) {
var remaining = number;
while (candidates.isNotEmpty && remaining > 0) {
assert(_cells.any((cell) => !cell.mine));
final index = rand.nextInt(candidates.length);
final (:row, :column) = candidates[index];
final cell = getCell(row: row, col: column);
if (!cell.mine) {
cell.mine = true;
_addRoundCellMineNum(row: row, col: col); // count as mine created
cnt += 1;
_addRoundCellMineNum(row: row, col: column); // count as mine created
remaining--;
candidates.removeAt(index);
}
}
}
Expand Down
12 changes: 9 additions & 3 deletions lib/game/minesweeper/entity/mode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ class GameMode {
name: "easy",
gameRows: defaultRows,
gameColumns: defaultColumns,
gameMines: 22,
gameMines: 18,
);
static const normal = GameMode._(
name: "normal",
gameRows: defaultRows,
gameColumns: defaultColumns,
gameMines: 35,
gameMines: 30,
);
static const hard = GameMode._(
name: "hard",
gameRows: defaultRows,
gameColumns: defaultColumns,
gameMines: 58,
gameMines: 43,
);

static final name2mode = {
Expand All @@ -30,6 +30,12 @@ class GameMode {
"hard": hard,
};

static final all = [
easy,
normal,
hard,
];

const GameMode._({
required this.name,
required this.gameRows,
Expand Down
2 changes: 1 addition & 1 deletion lib/game/minesweeper/entity/screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Screen {
}

double getInfoHeight() {
return (screenHeight - getBoardSize().height) * 0.2;
return (screenHeight - getBoardSize().height) * 0.3;
}

double getCellWidth() {
Expand Down
5 changes: 4 additions & 1 deletion lib/game/minesweeper/manager/logic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class GameLogic extends StateNotifier<GameStates> {
bool firstClick = true;
int mineNum = -1;

void initGame({required GameMode gameMode}) {
void initGame({required GameMode gameMode, bool notify = true}) {
state.mode = gameMode;
state.gameOver = false;
state.goodGame = false;
Expand All @@ -29,6 +29,9 @@ class GameLogic extends StateNotifier<GameStates> {
if (kDebugMode) {
logger.log(Level.info, "Game Init Finished");
}
if (notify) {
ref.notifyListeners();
}
}

// TODO: finish this
Expand Down
131 changes: 13 additions & 118 deletions lib/game/minesweeper/game.dart → lib/game/minesweeper/page/game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ import 'package:logger/logger.dart';
import 'package:rettulf/rettulf.dart';
import 'package:sit/design/adaptive/multiplatform.dart';
import 'package:sit/game/minesweeper/save.dart';
import 'entity/board.dart';
import 'entity/cell.dart';
import 'entity/mode.dart';
import 'widget/info.dart';
import 'manager/logic.dart';
import 'widget/board.dart';
import '../entity/board.dart';
import '../entity/mode.dart';
import '../widget/hud.dart';
import '../widget/info.dart';
import '../manager/logic.dart';
import '../widget/board.dart';
import "package:flutter/foundation.dart";
import 'manager/timer.dart';
import 'theme.dart';
import 'i18n.dart';
import '../manager/timer.dart';
import '../i18n.dart';

class GameMinesweeper extends ConsumerStatefulWidget {
final bool newGame;
Expand All @@ -30,22 +29,20 @@ class GameMinesweeper extends ConsumerStatefulWidget {

class _MinesweeperState extends ConsumerState<GameMinesweeper> with WidgetsBindingObserver {
late GameTimer timer;
late GameMode mode;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
mode = GameMode.easy;
timer = GameTimer(refresh: updateGame);
ref.read(boardManager.notifier).initGame(gameMode: mode);
Future.delayed(Duration.zero).then((value) {
ref.read(boardManager.notifier).initGame(gameMode: GameMode.easy,notify: false);
WidgetsBinding.instance.endOfFrame.then((_) {
if (!widget.newGame) {
final save = SaveMinesweeper.storage.load();
if (save != null) {
ref.read(boardManager.notifier).fromSave(Board.fromSave(save));
} else {
ref.read(boardManager.notifier).initGame(gameMode: mode);
ref.read(boardManager.notifier).initGame(gameMode: GameMode.easy);
}
}
});
Expand Down Expand Up @@ -92,9 +89,8 @@ class _MinesweeperState extends ConsumerState<GameMinesweeper> with WidgetsBindi

void resetGame({gameMode = GameMode.easy}) {
timer.stopTimer();
mode = gameMode;
timer = GameTimer(refresh: updateGame);
ref.read(boardManager.notifier).initGame(gameMode: mode);
ref.read(boardManager.notifier).initGame(gameMode: gameMode);
updateGame();
}

Expand All @@ -111,6 +107,7 @@ class _MinesweeperState extends ConsumerState<GameMinesweeper> with WidgetsBindi
Widget build(BuildContext context) {
// Get Your Screen Size
final screenSize = MediaQuery.of(context).size;
final mode = ref.watch(boardManager).mode;
initScreen(screenSize: screenSize, gameMode: mode);
// Build UI From Screen Size

Expand Down Expand Up @@ -147,105 +144,3 @@ class _MinesweeperState extends ConsumerState<GameMinesweeper> with WidgetsBindi
);
}
}

class MinesAndFlags extends StatelessWidget {
final int flags;
final int mines;

const MinesAndFlags({
super.key,
required this.flags,
required this.mines,
});

@override
Widget build(BuildContext context) {
final textTheme = context.textTheme;
return Row(
children: [
Text(
" $flags ",
style: textTheme.bodyLarge,
),
const Icon(
Icons.flag_outlined,
color: flagColor,
),
Text(
"/ $mines ",
style: textTheme.bodyLarge,
),
const Icon(
Icons.gps_fixed,
color: mineColor,
),
],
);
}
}

class GameHud extends ConsumerWidget {
final GameTimer timer;
final GameMode mode;

const GameHud({
super.key,
required this.mode,
required this.timer,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final screen = ref.read(boardManager).screen;
final boardRadius = screen.getBoardRadius();
final textTheme = context.textTheme;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: screen.getBoardSize().width / 2,
height: screen.getInfoHeight(),
decoration: BoxDecoration(
color: context.colorScheme.secondaryContainer,
borderRadius:
BorderRadius.only(topLeft: Radius.circular(boardRadius), bottomLeft: Radius.circular(boardRadius))),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const Icon(Icons.videogame_asset_outlined),
ref.read(boardManager).board.started
? MinesAndFlags(
flags: ref.read(boardManager).board.countAllByState(state: CellState.flag),
mines: ref.read(boardManager).board.mines,
)
: Text(
mode.l10n(),
style: textTheme.bodyLarge,
),
],
),
),
Container(
width: screen.getBoardSize().width / 2,
height: screen.getInfoHeight(),
decoration: BoxDecoration(
color: context.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.only(
topRight: Radius.circular(boardRadius),
bottomRight: Radius.circular(boardRadius),
)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const Icon(Icons.alarm),
Text(
timer.getTimeCost(),
style: textTheme.bodyLarge,
),
],
),
),
],
);
}
}
File renamed without changes.
Loading

0 comments on commit 8bc4321

Please sign in to comment.