Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve board generator #21

Merged
merged 9 commits into from
Mar 5, 2024
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/board_generator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ jobs:
dart_sdk: stable
working_directory: packages/board_generator
coverage_excludes: "**/*.g.dart"
min_coverage: 90
5 changes: 4 additions & 1 deletion packages/board_generator/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@
.dart_tool/
.packages
build/
pubspec.lock
pubspec.lock

allWords.json
board.txt
41 changes: 37 additions & 4 deletions packages/board_generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,50 @@ Crossword board generator.

## Usage 💻

**❗ In order to start using Board Generator you must have the [Dart SDK][dart_install_link] installed on your machine.**
There are two steps in the process to generate a board and have it ready in Firestore:

Run it as a dart program:
- Board generation
- Divide board into sections and upload it to Firestore

### Generate the crossword board

Add an `allWords.json` file inside the `assets` folder with all the words that will be used to generate the board with the following structure:

```json
{
"words":[
{
"answer": "alpine",
"definition": "Relative to high mountains"
}
]
}
```

Then, from the package folder, run:

```sh
dart lib/generate_board.dart
```

The board will be created in `assets/board.txt` and updated until all words are placed in it.

### Dividing the board in sections

Once the board is generated, create the sections and upload them to Firestore by running:

```sh
dart main.dart
dart lib/create_sections.dart
```

In case you want to update the sections size, do so in the `create_sections.dart` file before creating the sections.

>Note: When creating the sections you will need a service account to upload them to Firestore. Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the path of your service account.

## Running Tests 🧪

The coverage in this package is not enforced at a 100% due to the nature of testing the algorithm with only a few words.

To run all unit tests:

```sh
Expand All @@ -36,7 +70,6 @@ genhtml coverage/lcov.info -o coverage/
open coverage/index.html
```

[dart_install_link]: https://dart.dev/get-dart
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
Expand Down
24 changes: 0 additions & 24 deletions packages/board_generator/lib/board_generator.dart

This file was deleted.

119 changes: 119 additions & 0 deletions packages/board_generator/lib/create_sections.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// ignore_for_file: avoid_print

import 'dart:io';

import 'package:board_generator/src/crossword_repository.dart';
import 'package:csv/csv.dart';
import 'package:dart_firebase_admin/dart_firebase_admin.dart';
import 'package:dart_firebase_admin/firestore.dart';
import 'package:game_domain/game_domain.dart';

void main(List<String> args) async {
final serviceAccountPath =
Platform.environment['GOOGLE_APPLICATION_CREDENTIALS'];
if (serviceAccountPath == null) {
throw Exception('Service account path not found');
}

final admin = FirebaseAdminApp.initializeApp(
'io-crossword-dev',
Credential.fromServiceAccount(File(serviceAccountPath)),
);
final firestore = Firestore(admin);
final crosswordRepository = CrosswordRepository(firestore: firestore);

// Read the file
final fileString = File('assets/board.txt').readAsStringSync();
final rows = const CsvToListConverter(eol: '\n').convert(fileString);

// Convert to custom object
final words = rows.map((row) {
return Word(
position: Point(row[0] as int, row[1] as int),
answer: row[2] as String,
clue: 'The answer is: ${row[2]}',
hints: const [],
visible: false,
axis: row[3] == 'horizontal' ? Axis.horizontal : Axis.vertical,
solvedTimestamp: null,
);
}).toList();

// Get crossword size
final maxX = words
.map((e) => e.position.x)
.reduce((value, element) => value > element ? value : element);
final maxY = words
.map((e) => e.position.y)
.reduce((value, element) => value > element ? value : element);
final minX = words
.map((e) => e.position.x)
.reduce((value, element) => value < element ? value : element);
final minY = words
.map((e) => e.position.y)
.reduce((value, element) => value < element ? value : element);

final boardHeight = maxY - minY;
final boardWidth = maxX - minX;

print('Crossword size: $boardWidth x $boardHeight.');

final sections = <BoardSection>[];
const sectionSize = 300;

var sectionX = minX;
while (sectionX < maxX) {
var sectionY = minY;
while (sectionY < maxY) {
final sectionWords = words.where((word) {
return word.isStartInSection(sectionX, sectionY, sectionSize);
}).toList();

final borderWords = words.where((word) {
final isStartInSection =
word.isStartInSection(sectionX, sectionY, sectionSize);
final isEndInSection =
word.isEndInSection(sectionX, sectionY, sectionSize);
return !isStartInSection && isEndInSection;
}).toList();

final section = BoardSection(
id: '',
position: Point(sectionX, sectionY),
size: sectionSize,
words: sectionWords,
borderWords: borderWords,
);
sections.add(section);

sectionY += sectionSize;
}
sectionX += sectionSize;
}

await crosswordRepository.addSections(sections);

print('Added all ${sections.length} section to the database.');
}

/// An extension on [Word] to check if it is in a section.
extension SectionBelonging on Word {
/// Returns true if the word starting letter is in the section.
bool isStartInSection(int sectionX, int sectionY, int sectionSize) {
return position.x >= sectionX &&
position.x < sectionX + sectionSize &&
position.y >= sectionY &&
position.y < sectionY + sectionSize;
}

/// Returns true if the word ending letter is in the section.
bool isEndInSection(int sectionX, int sectionY, int sectionSize) {
final (endX, endY) = axis == Axis.horizontal
? (position.x + answer.length - 1, position.y)
: (position.x, position.y + answer.length - 1);
return endX >= sectionX &&
endX < sectionX + sectionSize &&
endY >= sectionY &&
endY < sectionY + sectionSize;
}
}
23 changes: 23 additions & 0 deletions packages/board_generator/lib/generate_board.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'dart:convert';
import 'dart:io';

import 'package:board_generator/src/board_generator.dart';

void main(List<String> args) async {
final file = File('assets/allWords.json');

final string = await file.readAsString();
final map = jsonDecode(string) as Map<String, dynamic>;
final words =
(map['words'] as List<dynamic>).map((e) => e as Map<String, dynamic>);
final parsedWords = words.map((word) {
return word['answer'] as String;
}).toList();

final regex = RegExp(r'^[a-z]+$');
final filteredWords = [...parsedWords]..removeWhere(
(element) => !regex.hasMatch(element),
);

generateCrossword(filteredWords, 'assets/board.txt');
}
16 changes: 0 additions & 16 deletions packages/board_generator/lib/main.dart

This file was deleted.

Loading
Loading