Skip to content

Commit

Permalink
feat: Initial Crossword board (#16)
Browse files Browse the repository at this point in the history
* feat: initial crossword structure

* rebase fixes

* feat: Initial Crossword board

* rebase fixes

* tests

* fix lint
  • Loading branch information
erickzanardo authored Mar 4, 2024
1 parent 65d05d9 commit fafc849
Show file tree
Hide file tree
Showing 17 changed files with 978 additions and 24 deletions.
Binary file added assets/images/letters.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 73 additions & 3 deletions lib/crossword/bloc/crossword_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ part 'crossword_state.dart';
class CrosswordBloc extends Bloc<CrosswordEvent, CrosswordState> {
CrosswordBloc() : super(const CrosswordInitial()) {
on<InitialBoardLoadRequested>(_onInitialBoardLoadRequested);
on<BoardSectionRequested>(_onBoardSectionRequested);
}

Future<void> _onInitialBoardLoadRequested(
Expand All @@ -16,7 +17,7 @@ class CrosswordBloc extends Bloc<CrosswordEvent, CrosswordState> {
) async {
const section = BoardSection(
id: '1',
position: Point(0, 0),
position: Point(2, 2),
width: 40,
height: 40,
words: [
Expand All @@ -34,11 +35,80 @@ class CrosswordBloc extends Bloc<CrosswordEvent, CrosswordState> {
);

emit(
const CrosswordLoaded(
CrosswordLoaded(
width: 40,
height: 40,
sections: [section],
sectionSize: 400,
sections: {
(section.position.x, section.position.y): section,
},
),
);
}

Future<void> _onBoardSectionRequested(
BoardSectionRequested event,
Emitter<CrosswordState> emit,
) async {
final loadedState = state;
if (loadedState is CrosswordLoaded) {
final section = BoardSection(
id: '',
position: Point(event.position.$1, event.position.$2),
width: 40,
height: 40,
words: const [
Word(
id: '',
axis: Axis.horizontal,
position: Point(0, 0),
answer: 'flutter',
clue: 'flutter',
hints: ['dart', 'mobile', 'cross-platform'],
visible: true,
solvedTimestamp: null,
),
Word(
id: '',
axis: Axis.vertical,
position: Point(4, 1),
answer: 'android',
clue: 'flutter',
hints: ['dart', 'mobile', 'cross-platform'],
visible: true,
solvedTimestamp: null,
),
Word(
id: '',
axis: Axis.vertical,
position: Point(8, 3),
answer: 'dino',
clue: 'flutter',
hints: ['dart', 'mobile', 'cross-platform'],
visible: true,
solvedTimestamp: null,
),
Word(
id: '',
position: Point(4, 6),
axis: Axis.horizontal,
answer: 'sparky',
clue: 'flutter',
hints: ['dart', 'mobile', 'cross-platform'],
visible: true,
solvedTimestamp: null,
),
],
);

emit(
loadedState.copyWith(
sections: {
...loadedState.sections,
(section.position.x, section.position.y): section,
},
),
);
}
}
}
9 changes: 9 additions & 0 deletions lib/crossword/bloc/crossword_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ class InitialBoardLoadRequested extends CrosswordEvent {
@override
List<Object> get props => [];
}

class BoardSectionRequested extends CrosswordEvent {
const BoardSectionRequested(this.position);

final (int, int) position;

@override
List<Object> get props => [position];
}
20 changes: 18 additions & 2 deletions lib/crossword/bloc/crossword_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,31 @@ class CrosswordLoaded extends CrosswordState {
const CrosswordLoaded({
required this.width,
required this.height,
required this.sectionSize,
required this.sections,
});

final int width;
final int height;
final List<BoardSection> sections;
final int sectionSize;
final Map<(int, int), BoardSection> sections;

CrosswordLoaded copyWith({
int? width,
int? height,
int? sectionSize,
Map<(int, int), BoardSection>? sections,
}) {
return CrosswordLoaded(
width: width ?? this.width,
height: height ?? this.height,
sectionSize: sectionSize ?? this.sectionSize,
sections: sections ?? this.sections,
);
}

@override
List<Object> get props => [width, height, sections];
List<Object> get props => [width, height, sectionSize, sections];
}

class CrosswordError extends CrosswordState {
Expand Down
1 change: 1 addition & 0 deletions lib/crossword/game/components/components.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'section_component.dart';
88 changes: 88 additions & 0 deletions lib/crossword/game/components/section_component.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'dart:async';

import 'package:flame/components.dart';
import 'package:flame/sprite.dart';
import 'package:flutter/material.dart' hide Axis;
import 'package:game_domain/game_domain.dart';
import 'package:io_crossword/crossword/crossword.dart';

class SectionComponent extends PositionComponent
with HasGameRef<CrosswordGame> {
SectionComponent({
required this.index,
super.key,
});

final (int, int) index;

late StreamSubscription<CrosswordState> _subscription;

@override
FutureOr<void> onLoad() async {
await super.onLoad();

final boardSection = gameRef.state.sections[index];
if (boardSection != null) {
_loadBoardSection(boardSection);
} else {
_subscription = gameRef.bloc.stream.listen((state) {
if (state is CrosswordLoaded) {
final boardSection = state.sections[index];
if (boardSection != null) {
_subscription.cancel();
_loadBoardSection(boardSection);
}
}
});

gameRef.bloc.add(
BoardSectionRequested(index),
);
}
}

void _loadBoardSection(BoardSection section) {
final spriteBatch = SpriteBatch(gameRef.lettersSprite);

final sectionPosition = Vector2(
(index.$1 * gameRef.sectionSize).toDouble(),
(index.$2 * gameRef.sectionSize).toDouble(),
);

for (var i = 0; i < section.words.length; i++) {
final word = section.words[i];

final wordCharacters = word.answer.toUpperCase().characters;

for (var c = 0; c < wordCharacters.length; c++) {
final char = wordCharacters.elementAt(c);
final charIndex = char.codeUnitAt(0) - 65;
final rect = Rect.fromLTWH(
(charIndex * CrosswordGame.cellSize).toDouble(),
0,
CrosswordGame.cellSize.toDouble(),
CrosswordGame.cellSize.toDouble(),
);

final x = word.axis == Axis.horizontal
? word.position.x + c
: word.position.x;

final y =
word.axis == Axis.vertical ? word.position.y + c : word.position.y;
final offset = sectionPosition +
Vector2(
x * CrosswordGame.cellSize.toDouble(),
y * CrosswordGame.cellSize.toDouble(),
);

spriteBatch.add(
source: rect,
offset: offset,
);
}
}

add(SpriteBatchComponent(spriteBatch: spriteBatch));
}
}
139 changes: 138 additions & 1 deletion lib/crossword/game/crossword_game.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,140 @@
import 'dart:async';

import 'package:flame/camera.dart';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:io_crossword/crossword/crossword.dart';

class CrosswordGame extends FlameGame with PanDetector {
CrosswordGame(this.bloc);

static const cellSize = 40;

final CrosswordBloc bloc;

late final Size totalArea;
late final int sectionSize;

late final Image lettersSprite;

var _visibleSections = <(double, double)>[];

CrosswordLoaded get state {
final state = bloc.state;
if (state is! CrosswordLoaded) {
throw ArgumentError('Cannot load game without a loaded state.');
}
return state;
}

@override
FutureOr<void> onLoad() async {
await super.onLoad();

// TODO(erickzanardo): Use the assets cubit instead
lettersSprite = await images.load('letters.png');

sectionSize = state.sectionSize;

totalArea = Size(
(state.width * cellSize).toDouble(),
(state.height * cellSize).toDouble(),
);

camera
..priority = 1
..viewport = (MaxViewport()..anchor = Anchor.topLeft)
..viewfinder.position = Vector2(
totalArea.width / 2,
totalArea.height / 2,
);

_updateVisibleSections();
}

void _updateVisibleSections() {
final visibleViewport = camera.visibleWorldRect;

final viewportMiddle = visibleViewport.size / 2;

final horizontalSectionsVisibleInViewport =
(visibleViewport.width / sectionSize).ceilToDouble();
final verticalSectionsVisibleInViewport =
(visibleViewport.height / sectionSize).ceilToDouble();

final cameraPosition =
camera.viewfinder.position + viewportMiddle.toVector2();

final startSection = Vector2(
((cameraPosition.x - viewportMiddle.width) ~/ sectionSize).toDouble(),
((cameraPosition.y - viewportMiddle.height) ~/ sectionSize)
.toDouble(),
) -
Vector2(
horizontalSectionsVisibleInViewport,
verticalSectionsVisibleInViewport,
);

final endSection = Vector2(
((cameraPosition.x + viewportMiddle.width) ~/ sectionSize).toDouble(),
((cameraPosition.y + viewportMiddle.height) ~/ sectionSize)
.toDouble(),
) +
Vector2(
horizontalSectionsVisibleInViewport,
verticalSectionsVisibleInViewport,
);

final currentVisibleSections = <(double, double)>[];
for (var x = startSection.x; x <= endSection.x; x++) {
for (var y = startSection.y; y <= endSection.y; y++) {
final sectionIndex = (x, y);
currentVisibleSections.add(sectionIndex);
}
}

final sectionsToRemove =
_visibleSections.toSet().difference(currentVisibleSections.toSet());
final sectionsToAdd =
currentVisibleSections.toSet().difference(_visibleSections.toSet());

if (sectionsToRemove.isNotEmpty) {
for (final sectionIndex in sectionsToRemove) {
findByKeyName('section-$sectionIndex')?.removeFromParent();
}
}

if (sectionsToAdd.isNotEmpty) {
for (final sectionIndex in sectionsToAdd) {
world.add(
SectionComponent(
key: ComponentKey.named('section-$sectionIndex'),
index: (
sectionIndex.$1.toInt(),
sectionIndex.$2.toInt(),
),
),
);
}
}

_visibleSections = currentVisibleSections;
}

var _distanceMoved = 0.0;

@override
void onPanUpdate(DragUpdateInfo info) {
super.onPanUpdate(info);

_distanceMoved += info.delta.global.length;
camera.viewfinder.position -= info.delta.global;

class CrosswordGame extends FlameGame {}
if (_distanceMoved >= 280) {
_distanceMoved = 0;
_updateVisibleSections();
}
}
}
1 change: 1 addition & 0 deletions lib/crossword/game/game.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export 'components/components.dart';
export 'crossword_game.dart';
6 changes: 4 additions & 2 deletions lib/crossword/view/crossword_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ class CrosswordView extends StatelessWidget {
} else if (state is CrosswordError) {
child = const Center(child: Text('Error loading crossword'));
} else if (state is CrosswordLoaded) {
child = const GameWidget.controlled(
gameFactory: CrosswordGame.new,
child = GameWidget.controlled(
gameFactory: () => CrosswordGame(
context.read(),
),
);
}

Expand Down
Loading

0 comments on commit fafc849

Please sign in to comment.