diff --git a/api/packages/game_domain/build.yaml b/api/packages/game_domain/build.yaml new file mode 100644 index 000000000..aaa6e0dd2 --- /dev/null +++ b/api/packages/game_domain/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + builders: + json_serializable: + options: + explicit_to_json: true \ No newline at end of file diff --git a/api/packages/game_domain/lib/src/models/board_section.dart b/api/packages/game_domain/lib/src/models/board_section.dart new file mode 100644 index 000000000..2580fcf1d --- /dev/null +++ b/api/packages/game_domain/lib/src/models/board_section.dart @@ -0,0 +1,58 @@ +import 'package:equatable/equatable.dart'; +import 'package:game_domain/game_domain.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'board_section.g.dart'; + +/// {@template board_section} +/// A model that represents a board section with all the words that +/// it contains. +/// {@endtemplate} +@JsonSerializable(ignoreUnannotated: true) +class BoardSection extends Equatable { + /// {@macro board_section} + const BoardSection({ + required this.id, + required this.position, + required this.width, + required this.height, + required this.words, + }); + + /// {@macro board_section} + factory BoardSection.fromJson(Map json) => + _$BoardSectionFromJson(json); + + /// Unique identifier of board section. + @JsonKey() + final String id; + + /// Position of the board section in the board. The origin is the top left. + @JsonKey() + @PointConverter() + final Point position; + + /// Width of the board section. + @JsonKey() + final int width; + + /// Height of the board section. + @JsonKey() + final int height; + + /// The words that are contained in this board section. + @JsonKey() + final List words; + + /// Returns a json representation from this instance. + Map toJson() => _$BoardSectionToJson(this); + + @override + List get props => [ + id, + position, + width, + height, + words, + ]; +} diff --git a/api/packages/game_domain/lib/src/models/board_section.g.dart b/api/packages/game_domain/lib/src/models/board_section.g.dart new file mode 100644 index 000000000..6852544d9 --- /dev/null +++ b/api/packages/game_domain/lib/src/models/board_section.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'board_section.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BoardSection _$BoardSectionFromJson(Map json) => BoardSection( + id: json['id'] as String, + position: const PointConverter() + .fromJson(json['position'] as Map), + width: json['width'] as int, + height: json['height'] as int, + words: (json['words'] as List) + .map((e) => Word.fromJson(e as Map)) + .toList(), + ); + +Map _$BoardSectionToJson(BoardSection instance) => + { + 'id': instance.id, + 'position': const PointConverter().toJson(instance.position), + 'width': instance.width, + 'height': instance.height, + 'words': instance.words.map((e) => e.toJson()).toList(), + }; diff --git a/api/packages/game_domain/lib/src/models/models.dart b/api/packages/game_domain/lib/src/models/models.dart index 4c1c38f2b..a2478f4d4 100644 --- a/api/packages/game_domain/lib/src/models/models.dart +++ b/api/packages/game_domain/lib/src/models/models.dart @@ -1 +1,6 @@ +export 'dart:math' show Point; + +export 'board_section.dart'; export 'leaderboard_player.dart'; +export 'point_converter.dart'; +export 'word.dart'; diff --git a/api/packages/game_domain/lib/src/models/point_converter.dart b/api/packages/game_domain/lib/src/models/point_converter.dart new file mode 100644 index 000000000..b201d51e6 --- /dev/null +++ b/api/packages/game_domain/lib/src/models/point_converter.dart @@ -0,0 +1,26 @@ +import 'package:game_domain/game_domain.dart'; +import 'package:json_annotation/json_annotation.dart'; + +/// {@template point_converter} +/// A converter that converts a [Point] to a [Map] and vice versa. +/// {@endtemplate} +class PointConverter extends JsonConverter, Map> { + /// {@macro point_converter} + const PointConverter(); + + @override + Point fromJson(Map json) { + return Point( + json['x'] as int, + json['y'] as int, + ); + } + + @override + Map toJson(Point object) { + return { + 'x': object.x, + 'y': object.y, + }; + } +} diff --git a/api/packages/game_domain/lib/src/models/word.dart b/api/packages/game_domain/lib/src/models/word.dart new file mode 100644 index 000000000..08e56a349 --- /dev/null +++ b/api/packages/game_domain/lib/src/models/word.dart @@ -0,0 +1,71 @@ +import 'package:equatable/equatable.dart'; +import 'package:game_domain/game_domain.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'word.g.dart'; + +/// {@template word} +/// A model that represents a word in the crossword. +/// {@endtemplate} +@JsonSerializable(ignoreUnannotated: true) +class Word extends Equatable { + /// {@macro word} + const Word({ + required this.id, + required this.position, + required this.answer, + required this.clue, + required this.hints, + required this.visible, + required this.solvedTimestamp, + }); + + /// {@macro word} + factory Word.fromJson(Map json) => _$WordFromJson(json); + + /// Unique identifier of the word. + @JsonKey() + final String id; + + /// Position of the board section in the board. The origin is the top left. + @JsonKey() + @PointConverter() + final Point position; + + /// The word answer to display in the crossword when solved. + @JsonKey() + final String answer; + + /// The clue to show users when guessing for the first time. + @JsonKey() + final String clue; + + /// The hints to show users when asked for more hints. + @JsonKey() + final List hints; + + /// Whether the word should be visible or not in the board. Independent of + /// the word being solved or not. + /// Every solved word is visible, but not every visible word is solved. + @JsonKey() + final bool visible; + + /// The timestamp when the word was solved. In milliseconds since epoch. + /// If the word is not solved, this value is null. + @JsonKey() + final int? solvedTimestamp; + + /// Returns a json representation from this instance. + Map toJson() => _$WordToJson(this); + + @override + List get props => [ + id, + position, + answer, + clue, + hints, + visible, + solvedTimestamp, + ]; +} diff --git a/api/packages/game_domain/lib/src/models/word.g.dart b/api/packages/game_domain/lib/src/models/word.g.dart new file mode 100644 index 000000000..4ad00c40b --- /dev/null +++ b/api/packages/game_domain/lib/src/models/word.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'word.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Word _$WordFromJson(Map json) => Word( + id: json['id'] as String, + position: const PointConverter() + .fromJson(json['position'] as Map), + answer: json['answer'] as String, + clue: json['clue'] as String, + hints: (json['hints'] as List).map((e) => e as String).toList(), + visible: json['visible'] as bool, + solvedTimestamp: json['solvedTimestamp'] as int?, + ); + +Map _$WordToJson(Word instance) => { + 'id': instance.id, + 'position': const PointConverter().toJson(instance.position), + 'answer': instance.answer, + 'clue': instance.clue, + 'hints': instance.hints, + 'visible': instance.visible, + 'solvedTimestamp': instance.solvedTimestamp, + }; diff --git a/api/packages/game_domain/pubspec.yaml b/api/packages/game_domain/pubspec.yaml index 78f90dce0..469d61499 100644 --- a/api/packages/game_domain/pubspec.yaml +++ b/api/packages/game_domain/pubspec.yaml @@ -6,12 +6,13 @@ publish_to: none environment: sdk: ">=3.0.0 <4.0.0" +dependencies: + equatable: ^2.0.5 + json_annotation: ^4.8.1 + dev_dependencies: build_runner: ^2.4.8 - mocktail: ^1.0.0 + json_serializable: ^6.7.1 + mocktail: ^1.0.3 test: ^1.19.2 - very_good_analysis: ^5.1.0 - -dependencies: - equatable: ^2.0.5 - json_annotation: ^4.8.1 \ No newline at end of file + very_good_analysis: ^5.1.0 \ No newline at end of file diff --git a/api/packages/game_domain/test/src/models/board_section_test.dart b/api/packages/game_domain/test/src/models/board_section_test.dart new file mode 100644 index 000000000..9ddd1a6e6 --- /dev/null +++ b/api/packages/game_domain/test/src/models/board_section_test.dart @@ -0,0 +1,113 @@ +// ignore_for_file: prefer_const_constructors +// ignore_for_file: prefer_const_literals_to_create_immutables + +import 'package:game_domain/game_domain.dart'; +import 'package:test/test.dart'; + +void main() { + group('BoardSection', () { + test('creates correct json object from BoardSection object', () { + final boardSection = BoardSection( + id: 'id', + position: Point(1, 2), + width: 3, + height: 4, + words: [ + Word( + id: 'id', + position: Point(1, 2), + answer: 'answer', + clue: 'clue', + hints: ['hints'], + visible: false, + solvedTimestamp: 1234, + ), + ], + ); + final json = boardSection.toJson(); + + expect( + json, + equals({ + 'id': 'id', + 'position': {'x': 1, 'y': 2}, + 'width': 3, + 'height': 4, + 'words': [ + { + 'id': 'id', + 'position': {'x': 1, 'y': 2}, + 'answer': 'answer', + 'clue': 'clue', + 'hints': ['hints'], + 'visible': false, + 'solvedTimestamp': 1234, + }, + ], + }), + ); + }); + + test('creates correct BoardSection object from json object', () { + final json = { + 'id': 'id', + 'position': {'x': 1, 'y': 2}, + 'width': 3, + 'height': 4, + 'words': [ + { + 'id': 'id', + 'position': {'x': 1, 'y': 2}, + 'answer': 'answer', + 'clue': 'clue', + 'hints': ['hints'], + 'visible': false, + 'solvedTimestamp': 1234, + }, + ], + }; + final boardSection = BoardSection.fromJson(json); + expect( + boardSection, + equals( + BoardSection( + id: 'id', + position: Point(1, 2), + width: 3, + height: 4, + words: [ + Word( + id: 'id', + position: Point(1, 2), + answer: 'answer', + clue: 'clue', + hints: ['hints'], + visible: false, + solvedTimestamp: 1234, + ), + ], + ), + ), + ); + }); + + test('supports equality', () { + final firstBoardSection = BoardSection( + id: 'id', + position: Point(1, 2), + width: 3, + height: 4, + words: [], + ); + final secondBoardSection = BoardSection( + id: 'id', + position: Point(1, 2), + width: 3, + height: 4, + words: [], + ); + + expect(firstBoardSection, equals(secondBoardSection)); + }); + }); +} diff --git a/api/packages/game_domain/test/src/models/point_converter_test.dart b/api/packages/game_domain/test/src/models/point_converter_test.dart new file mode 100644 index 000000000..58ebd3500 --- /dev/null +++ b/api/packages/game_domain/test/src/models/point_converter_test.dart @@ -0,0 +1,20 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:game_domain/game_domain.dart'; +import 'package:test/test.dart'; + +void main() { + group('PointConverter', () { + test('creates correct json object from Point object', () { + final point = Point(1, 2); + final json = PointConverter().toJson(point); + expect(json, equals({'x': 1, 'y': 2})); + }); + + test('creates correct Point object from json object', () { + final json = {'x': 1, 'y': 2}; + final point = PointConverter().fromJson(json); + expect(point, equals(Point(1, 2))); + }); + }); +} diff --git a/api/packages/game_domain/test/src/models/word_test.dart b/api/packages/game_domain/test/src/models/word_test.dart new file mode 100644 index 000000000..f1dfbe182 --- /dev/null +++ b/api/packages/game_domain/test/src/models/word_test.dart @@ -0,0 +1,83 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:game_domain/game_domain.dart'; +import 'package:test/test.dart'; + +void main() { + group('Word', () { + test('creates correct json object from Word object', () { + final word = Word( + id: 'id', + position: Point(1, 2), + answer: 'test', + clue: 'clue', + hints: const ['hint'], + visible: true, + solvedTimestamp: 0, + ); + final json = word.toJson(); + + expect( + json, + equals({ + 'id': 'id', + 'position': {'x': 1, 'y': 2}, + 'answer': 'test', + 'clue': 'clue', + 'hints': ['hint'], + 'visible': true, + 'solvedTimestamp': 0, + }), + ); + }); + + test('creates correct Word object from json object', () { + final json = { + 'id': 'id', + 'position': {'x': 1, 'y': 2}, + 'answer': 'test', + 'clue': 'clue', + 'hints': ['hint'], + 'visible': true, + }; + final word = Word.fromJson(json); + expect( + word, + equals( + Word( + id: 'id', + position: Point(1, 2), + answer: 'test', + clue: 'clue', + hints: const ['hint'], + visible: true, + solvedTimestamp: null, + ), + ), + ); + }); + + test('supports equality', () { + final firstWord = Word( + id: 'id', + position: Point(1, 2), + answer: 'test', + clue: 'clue', + hints: const ['hint'], + visible: true, + solvedTimestamp: 0, + ); + final secondWord = Word( + id: 'id', + position: Point(1, 2), + answer: 'test', + clue: 'clue', + hints: const ['hint'], + visible: true, + solvedTimestamp: 0, + ); + + expect(firstWord, equals(secondWord)); + }); + }); +}