Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions lib/src/game/field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,42 @@ class Field extends Array2d<bool> {
return Field._internal(bombCount, cols, squares);
}

/// Creates a field with a guaranteed safe position at (safeX, safeY)
/// This ensures the first click in Minesweeper is never a bomb
factory Field.withSafePosition(
int bombCount,
int cols,
int rows,
int safeX,
int safeY, [
int? seed,
]) {
final squares = List<bool>.filled(rows * cols, false);
assert(bombCount < squares.length);
assert(bombCount > 0);
assert(safeX >= 0 && safeX < cols);
assert(safeY >= 0 && safeY < rows);

final rnd = Random(seed);
final safeIndex = safeY * cols + safeX;

// create a list of all valid positions (excluding safe position)
final availablePositions = <int>[];
for (int i = 0; i < squares.length; i++) {
if (i != safeIndex) {
availablePositions.add(i);
}
}

// Shuffle the available positions and place bombs in first X positions
availablePositions.shuffle(rnd);
for (int i = 0; i < bombCount; i++) {
squares[availablePositions[i]] = true;
}

return Field._internal(bombCount, cols, squares);
}

factory Field.fromSquares(int cols, int rows, List<bool> squares) {
assert(cols > 0);
assert(rows > 0);
Expand Down
50 changes: 45 additions & 5 deletions lib/src/game/game_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ enum SquareState { hidden, revealed, flagged, bomb, safe }
enum GameState { reset, started, won, lost }

class Game {
final Field field;
Field? _field; // Now nullable - will be created on first click
final int width, height, bombCount; // Store dimensions instead
final Array2d<SquareState> _states;
final _updatedEvent = StreamController<void>();
final _gameStateEvent = StreamController<GameState>();
Expand All @@ -23,8 +24,20 @@ class Game {
int _bombsLeft;
int _revealsLeft;

Game(this.field)
// Constructor takes dimensions instead of Field
Game(this.width, this.height, this.bombCount)
: _state = GameState.reset,
_states = Array2d<SquareState>(width, height, (i) => SquareState.hidden),
_bombsLeft = bombCount,
_revealsLeft = width * height - bombCount;

// Legacy constructor for backward compatibility with existing Field
Game.fromField(Field field)
: _field = field,
width = field.width,
height = field.height,
bombCount = field.bombCount,
_state = GameState.reset,
_states = Array2d<SquareState>(
field.width,
field.height,
Expand All @@ -33,6 +46,16 @@ class Game {
_bombsLeft = field.bombCount,
_revealsLeft = field.length - field.bombCount;

// Getter: Access field safely - will be generated on first reveal
Field get field {
if (_field == null) {
// For UI that needs field before first click, create a temporary empty field
// This won't be used for actual gameplay logic
return Field(bombCount, width, height);
}
return _field!;
}

int get bombsLeft => _bombsLeft;

int get revealsLeft => _revealsLeft;
Expand All @@ -56,7 +79,7 @@ class Game {
}

void setFlag(int x, int y, bool value) {
_ensureStarted();
_ensureStarted(); // Don't pass coordinates for flag - use fallback field generation

final currentSS = _states.get(x, y);
if (value) {
Expand All @@ -82,7 +105,7 @@ class Game {
}

List<Point<int>>? reveal(int x, int y) {
_ensureStarted();
_ensureStarted(x, y); // Pass coordinates for safe first click
assert(canReveal(x, y), 'Item cannot be revealed.');
final currentSS = _states.get(x, y);

Expand Down Expand Up @@ -271,8 +294,25 @@ class Game {
}
}

void _ensureStarted() {
void _ensureStarted([int? firstClickX, int? firstClickY]) {
if (state == GameState.reset) {
// LAZY FIELD GENERATION: Create field on first click
if (_field == null) {
if (firstClickX != null && firstClickY != null) {
// Generate field with the first click position guaranteed safe
_field = Field.withSafePosition(
bombCount,
width,
height,
firstClickX,
firstClickY,
);
} else {
// Fallback: generate normal field (shouldn't happen in normal gameplay)
_field = Field(bombCount, width, height);
}
}

assert(!_watch.isRunning);
_setState(GameState.started);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/src/game_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ abstract class GameManager {
}

void _newGame() {
final f = Field(_bombCount, _width, _height);
_game = Game(f);
// Pass dimensions - let Game handle field creation lazily
_game = Game(_width, _height, _bombCount);
_gameStateChangedSub = _game.stateChanged.listen(_gameStateChanged);
}

Expand Down
6 changes: 3 additions & 3 deletions lib/src/game_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ class GameStorage {
bool updateBestTime(Game game) {
assert(game.state == GameState.won);

final w = game.field.width;
final h = game.field.height;
final m = game.field.bombCount;
final w = game.width;
final h = game.height;
final m = game.bombCount;
final duration = game.duration!.inMilliseconds;

final key = _getKey(w, h, m);
Expand Down
8 changes: 3 additions & 5 deletions lib/src/stage/board_element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ class BoardElement extends Sprite {
addTo(gameElement);

final scaledSize = SquareElement.size * _boardScale;
_elements = Array2d<SquareElement>(game.field.width, game.field.height, (
i,
) {
final x = i % game.field.width;
final y = i ~/ game.field.height;
_elements = Array2d<SquareElement>(game.width, game.height, (i) {
final x = i % game.width;
final y = i ~/ game.width;
return SquareElement(x, y)
..x = x * scaledSize
..y = y * scaledSize
Expand Down
2 changes: 1 addition & 1 deletion lib/src/stage/game_background_element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class GameBackgroundElement extends Sprite {
);
final tbr = Rectangle<int>(0, 0, 80, 112);
final lrr = Rectangle<int>(0, 0, 112, 80);
for (var i = 0; i < _game.field.width - 2; i++) {
for (var i = 0; i < _game.width - 2; i++) {
boardData
..drawPixels(
op.getBitmapData('game_board_side_top'),
Expand Down
2 changes: 1 addition & 1 deletion lib/src/stage/game_element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class GameElement extends Sprite {
final sta = resourceManager.getTextureAtlas('static');
_animations = resourceManager.getTextureAtlas('animated');

_boardSize = game.field.width * SquareElement.size + 2 * _edgeOffset;
_boardSize = game.width * SquareElement.size + 2 * _edgeOffset;
_boardScale = _backgroundHoleSize / _boardSize;

GameBackgroundElement(this, opa);
Expand Down
Loading
Loading