Skip to content

Commit

Permalink
feat: improve board generator (#21)
Browse files Browse the repository at this point in the history
* feat: update word placement algo

* feat: update board saving and logging

* feat: save board to firestore

* chore: add models package

* feat: update board generation

* feat: create and upload sections

* docs: update readme

* feat: improve readability

* chore: update min coverage
  • Loading branch information
jsgalarraga authored Mar 5, 2024
1 parent df89b89 commit 0dd3900
Show file tree
Hide file tree
Showing 15 changed files with 874 additions and 339 deletions.
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

0 comments on commit 0dd3900

Please sign in to comment.