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: board full render endpoint #45

Merged
merged 7 commits into from
Mar 8, 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
20 changes: 20 additions & 0 deletions .github/workflows/api_crossword_repository.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: api_crossword_repository

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
pull_request:
paths:
- "api/packages/crossword_repository/**"
- ".github/workflows/api_crossword_repository.yaml"
branches:
- main

jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1
with:
dart_sdk: stable
working_directory: api/packages/crossword_repository
2 changes: 2 additions & 0 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ COPY packages/encryption_middleware ./packages/encryption_middleware
COPY packages/game_domain ./packages/game_domain
COPY packages/jwt_middleware ./packages/jwt_middleware
COPY packages/leaderboard_repository ./packages/leaderboard_repository
COPY packages/board_renderer ./packages/board_renderer
COPY packages/crossword_repository ./packages/crossword_repository

# Install Dependencies
RUN dart pub get -C packages/db_client
Expand Down
17 changes: 12 additions & 5 deletions api/main.dart
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import 'dart:io';

import 'package:board_renderer/board_renderer.dart';
import 'package:crossword_repository/crossword_repository.dart';
import 'package:dart_frog/dart_frog.dart';
import 'package:db_client/db_client.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:logging/logging.dart';

late CrosswordRepository crosswordRepository;
late BoardRenderer boardRenderer;
late LeaderboardRepository leaderboardRepository;

Future<HttpServer> run(Handler handler, InternetAddress ip, int port) async {
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print('${record.level.name}: ${record.time}: ${record.message}');
});

final dbClient = DbClient.initialize(_appId, useEmulator: _useEmulator);

crosswordRepository = CrosswordRepository(dbClient: dbClient);
boardRenderer = const BoardRenderer();

leaderboardRepository = LeaderboardRepository(
dbClient: dbClient,
blacklistDocumentId: _initialsBlacklistId,
);

Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print('${record.level.name}: ${record.time}: ${record.message}');
});

return serve(handler, ip, port);
}

Expand Down
7 changes: 7 additions & 0 deletions api/packages/board_renderer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# See https://www.dartlang.org/guides/libraries/private-files

# Files and directories created by pub
.dart_tool/
.packages
build/
pubspec.lock
62 changes: 62 additions & 0 deletions api/packages/board_renderer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Board Renderer

[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason)
[![License: MIT][license_badge]][license_link]

Renders the board in images

## Installation 💻

**❗ In order to start using Board Renderer you must have the [Dart SDK][dart_install_link] installed on your machine.**

Install via `dart pub add`:

```sh
dart pub add board_renderer
```

---

## Continuous Integration 🤖

Board Renderer comes with a built-in [GitHub Actions workflow][github_actions_link] powered by [Very Good Workflows][very_good_workflows_link] but you can also add your preferred CI/CD solution.

Out of the box, on each pull request and push, the CI `formats`, `lints`, and `tests` the code. This ensures the code remains consistent and behaves correctly as you add functionality or make changes. The project uses [Very Good Analysis][very_good_analysis_link] for a strict set of analysis options used by our team. Code coverage is enforced using the [Very Good Workflows][very_good_coverage_link].

---

## Running Tests 🧪

To run all unit tests:

```sh
dart pub global activate coverage 1.2.0
dart test --coverage=coverage
dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info
```

To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov).

```sh
# Generate Coverage Report
genhtml coverage/lcov.info -o coverage/

# Open Coverage Report
open coverage/index.html
```

[dart_install_link]: https://dart.dev/get-dart
[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only
[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only
[mason_link]: https://github.com/felangel/mason
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis
[very_good_coverage_link]: https://github.com/marketplace/actions/very-good-coverage
[very_good_ventures_link]: https://verygood.ventures
[very_good_ventures_link_light]: https://verygood.ventures#gh-light-mode-only
[very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only
[very_good_workflows_link]: https://github.com/VeryGoodOpenSource/very_good_workflows
1 change: 1 addition & 0 deletions api/packages/board_renderer/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.5.1.0.yaml
20 changes: 20 additions & 0 deletions api/packages/board_renderer/coverage_badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions api/packages/board_renderer/lib/board_renderer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// Renders the board in images
library;

export 'src/board_renderer.dart';
146 changes: 146 additions & 0 deletions api/packages/board_renderer/lib/src/board_renderer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:game_domain/game_domain.dart';
import 'package:image/image.dart' as img;

/// A function that creates a command to execute.
typedef CreateCommand = img.Command Function();

/// A function that creates an image.
typedef CreateImage = img.Image Function({
required int width,
required int height,
});

/// A function that draws a rectangle in an image.
typedef DrawRect = img.Image Function(
img.Image dst, {
required int x1,
required int y1,
required int x2,
required int y2,
required img.Color color,
num thickness,
num radius,
img.Image? mask,
img.Channel maskChannel,
});

/// {@template board_renderer_failure}
/// Exception thrown when a board rendering fails.
/// {@endtemplate}
class BoardRendererFailure implements Exception {
/// {@macro board_renderer_failure}
BoardRendererFailure(this.message);

/// Message describing the failure.
final String message;

@override
String toString() => '[BoardRendererFailure]: $message';
}

/// {@template board_renderer}
/// Renders the board in images
/// {@endtemplate}
class BoardRenderer {
/// {@macro board_renderer}
const BoardRenderer({
CreateCommand createCommand = img.Command.new,
CreateImage createImage = img.Image.new,
DrawRect drawRect = img.drawRect,
}) : _createCommand = createCommand,
_createImage = createImage,
_drawRect = drawRect;

final CreateCommand _createCommand;
final CreateImage _createImage;
final DrawRect _drawRect;

/// The size of each cell in the board.
static const cellSize = 4;

/// Renders the full board in a single image.
Future<Uint8List> renderBoard(List<Word> words) async {
var minPositionX = 0;
var minPositionY = 0;

var maxPositionX = 0;
var maxPositionY = 0;

final color = img.ColorRgb8(255, 255, 255);

for (final word in words) {
minPositionX = math.min(minPositionX, word.position.x);
minPositionY = math.min(minPositionY, word.position.y);

final sizeX = word.axis == Axis.horizontal
? word.position.x + word.answer.length
: word.position.x;

final sizeY = word.axis == Axis.vertical
? word.position.y + word.answer.length
: word.position.y;

maxPositionX = math.max(maxPositionX, word.position.x + sizeX);
maxPositionY = math.max(maxPositionY, word.position.y + sizeY);
}

final totalWidth = (maxPositionX + minPositionX.abs()) * cellSize;
final totalHeight = (maxPositionY + minPositionY.abs()) * cellSize;

final centerX = (totalWidth / 2).round();
final centerY = (totalHeight / 2).round();

final image = _createImage(
width: totalWidth + cellSize,
height: totalHeight + cellSize,
);

for (final word in words) {
final wordPosition = (
word.position.x * cellSize,
word.position.y * cellSize,
);

final isHorizontal = word.axis == Axis.horizontal;
final wordCharacters = word.answer.split('');

for (var i = 0; i < wordCharacters.length; i++) {
_drawRect(
image,
x1: (isHorizontal
? wordPosition.$1 + i * cellSize
: wordPosition.$1) +
centerX,
y1: (isHorizontal
? wordPosition.$2
: wordPosition.$2 + i * cellSize) +
centerY,
x2: (isHorizontal
? wordPosition.$1 + i * cellSize + cellSize
: wordPosition.$1 + cellSize) +
centerX,
y2: (isHorizontal
? wordPosition.$2 + cellSize
: wordPosition.$2 + i * cellSize + cellSize) +
centerY,
color: color,
);
}
}

final createdCommand = _createCommand()
..image(image)
..encodePng();

await createdCommand.execute();

final outputBytes = createdCommand.outputBytes;
if (outputBytes == null) {
throw BoardRendererFailure('Failed to render the board');
}

return outputBytes;
}
}
17 changes: 17 additions & 0 deletions api/packages/board_renderer/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: board_renderer
description: Renders the board in images
version: 0.1.0+1
publish_to: none

environment:
sdk: "^3.3.0"

dependencies:
game_domain:
path: ../game_domain
image: ^4.1.7

dev_dependencies:
mocktail: ^1.0.3
test: ^1.25.2
very_good_analysis: ^5.1.0
Loading
Loading