diff --git a/api/packages/crossword_repository/lib/src/crossword_repository.dart b/api/packages/crossword_repository/lib/src/crossword_repository.dart index 53e7c2b5f..f8982bdf6 100644 --- a/api/packages/crossword_repository/lib/src/crossword_repository.dart +++ b/api/packages/crossword_repository/lib/src/crossword_repository.dart @@ -97,8 +97,10 @@ class CrosswordRepository { return false; } - final sectionX = correctAnswer.section.x; - final sectionY = correctAnswer.section.y; + // TODO(Ayad): update all the sections + // https://very-good-ventures-team.monday.com/boards/6004820050/pulses/6530206382 + final sectionX = correctAnswer.sections.first.x; + final sectionY = correctAnswer.sections.first.y; final section = await findSectionByPosition(sectionX, sectionY); if (section == null) { diff --git a/api/packages/crossword_repository/test/src/crossword_repository_test.dart b/api/packages/crossword_repository/test/src/crossword_repository_test.dart index da5a94a3d..9335c4400 100644 --- a/api/packages/crossword_repository/test/src/crossword_repository_test.dart +++ b/api/packages/crossword_repository/test/src/crossword_repository_test.dart @@ -185,7 +185,10 @@ void main() { when(() => answersRecord.id).thenReturn('1'); when(() => answersRecord.data).thenReturn({ 'answer': 'flutter', - 'section': {'x': 1, 'y': 1}, + 'sections': [ + {'x': 1, 'y': 1}, + ], + 'collidedWords': >[], }); when( () => dbClient.getById(answersCollection, '1'), @@ -271,7 +274,10 @@ void main() { when(() => answersRecord.id).thenReturn('fake'); when(() => answersRecord.data).thenReturn({ 'answer': 'flutter', - 'section': {'x': 1, 'y': 1}, + 'sections': [ + {'x': 1, 'y': 1}, + ], + 'collidedWords': >[], }); when( () => dbClient.getById(answersCollection, 'fake'), diff --git a/api/packages/game_domain/lib/src/models/answer.dart b/api/packages/game_domain/lib/src/models/answer.dart index 630840ee9..b0adeab2f 100644 --- a/api/packages/game_domain/lib/src/models/answer.dart +++ b/api/packages/game_domain/lib/src/models/answer.dart @@ -13,7 +13,8 @@ class Answer extends Equatable { const Answer({ required this.id, required this.answer, - required this.section, + required this.sections, + required this.collidedWords, }); /// {@macro answer} @@ -29,14 +30,18 @@ class Answer extends Equatable { @JsonKey() final String answer; - /// The section of the board where the word is located. + /// The sections of the board where the word is located. @JsonKey() - @PointConverter() - final Point section; + @ListPointConverter() + final List> sections; + + /// The words that collide with the current [Answer]. + @JsonKey() + final List collidedWords; /// Returns a json representation from this instance. Map toJson() => _$AnswerToJson(this); @override - List get props => [id, answer, section]; + List get props => [id, answer, sections, collidedWords]; } diff --git a/api/packages/game_domain/lib/src/models/answer.g.dart b/api/packages/game_domain/lib/src/models/answer.g.dart index 5bb5283df..a04df49ff 100644 --- a/api/packages/game_domain/lib/src/models/answer.g.dart +++ b/api/packages/game_domain/lib/src/models/answer.g.dart @@ -9,11 +9,15 @@ part of 'answer.dart'; Answer _$AnswerFromJson(Map json) => Answer( id: json['id'] as String, answer: json['answer'] as String, - section: const PointConverter() - .fromJson(json['section'] as Map), + sections: const ListPointConverter() + .fromJson(json['sections'] as List>), + collidedWords: (json['collidedWords'] as List) + .map((e) => CollidedWord.fromJson(e as Map)) + .toList(), ); Map _$AnswerToJson(Answer instance) => { 'answer': instance.answer, - 'section': const PointConverter().toJson(instance.section), + 'sections': const ListPointConverter().toJson(instance.sections), + 'collidedWords': instance.collidedWords.map((e) => e.toJson()).toList(), }; diff --git a/api/packages/game_domain/lib/src/models/collided_word.dart b/api/packages/game_domain/lib/src/models/collided_word.dart new file mode 100644 index 000000000..bc79d4e27 --- /dev/null +++ b/api/packages/game_domain/lib/src/models/collided_word.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:game_domain/game_domain.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'collided_word.g.dart'; + +/// {@template collided_word} +/// A model that represents collision of a word indicating the [wordId], the +/// [position] of the character and the [character]. +/// {@endtemplate} +@JsonSerializable() +class CollidedWord extends Equatable { + /// {@macro collided_word} + const CollidedWord({ + required this.wordId, + required this.position, + required this.character, + }); + + /// {@macro collided_word} + factory CollidedWord.fromJson(Map json) => + _$CollidedWordFromJson(json); + + /// Returns a json representation from this instance. + Map toJson() => _$CollidedWordToJson(this); + + /// The id of [Word]. + @JsonKey() + final String wordId; + + /// The position of the character where it collided. + /// First character 0, second character 1, etc. + @JsonKey() + final int position; + + /// The character that collided. + @JsonKey() + final String character; + + @override + List get props => [wordId, position, character]; +} diff --git a/api/packages/game_domain/lib/src/models/collided_word.g.dart b/api/packages/game_domain/lib/src/models/collided_word.g.dart new file mode 100644 index 000000000..1cbd12e92 --- /dev/null +++ b/api/packages/game_domain/lib/src/models/collided_word.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'collided_word.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CollidedWord _$CollidedWordFromJson(Map json) => CollidedWord( + wordId: json['wordId'] as String, + position: (json['position'] as num).toInt(), + character: json['character'] as String, + ); + +Map _$CollidedWordToJson(CollidedWord instance) => + { + 'wordId': instance.wordId, + 'position': instance.position, + 'character': instance.character, + }; diff --git a/api/packages/game_domain/lib/src/models/models.dart b/api/packages/game_domain/lib/src/models/models.dart index aa2297e23..8beff0e48 100644 --- a/api/packages/game_domain/lib/src/models/models.dart +++ b/api/packages/game_domain/lib/src/models/models.dart @@ -2,6 +2,7 @@ export 'dart:math' show Point; export 'answer.dart'; export 'board_section.dart'; +export 'collided_word.dart'; export 'hint.dart'; export 'mascots.dart'; export 'player.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 index 50f9e01a5..2f1285f12 100644 --- a/api/packages/game_domain/lib/src/models/point_converter.dart +++ b/api/packages/game_domain/lib/src/models/point_converter.dart @@ -24,3 +24,37 @@ class PointConverter extends JsonConverter, Map> { }; } } + +/// {@template point_converter} +/// A converter that converts a [List] of [Point] to a [List] of [Map] +/// and vice versa. +/// {@endtemplate} +class ListPointConverter + extends JsonConverter>, List>> { + /// {@macro point_converter} + const ListPointConverter(); + + @override + List> fromJson(List> json) { + return json + .map( + (j) => Point( + (j['x'] as num).toInt(), + (j['y'] as num).toInt(), + ), + ) + .toList(); + } + + @override + List> toJson(List> object) { + return object + .map( + (o) => { + 'x': o.x, + 'y': o.y, + }, + ) + .toList(); + } +} diff --git a/api/packages/game_domain/test/src/models/answer_test.dart b/api/packages/game_domain/test/src/models/answer_test.dart index 4f98cf16c..fcbc763f5 100644 --- a/api/packages/game_domain/test/src/models/answer_test.dart +++ b/api/packages/game_domain/test/src/models/answer_test.dart @@ -6,14 +6,34 @@ import 'package:test/test.dart'; void main() { group('Answer', () { test('creates correct json object from Answer object', () { - final answer = Answer(id: 'id', answer: 'answer', section: Point(1, 2)); + final answer = Answer( + id: 'id', + answer: 'answer', + sections: const [Point(1, 2)], + collidedWords: const [ + CollidedWord( + wordId: 'word-id', + position: 3, + character: 'b', + ), + ], + ); final json = answer.toJson(); expect( json, equals({ 'answer': 'answer', - 'section': {'x': 1, 'y': 2}, + 'sections': [ + {'x': 1, 'y': 2}, + ], + 'collidedWords': [ + { + 'wordId': 'word-id', + 'position': 3, + 'character': 'b', + }, + ], }), ); }); @@ -22,13 +42,33 @@ void main() { final json = { 'id': 'id', 'answer': 'answer', - 'section': {'x': 1, 'y': 2}, + 'sections': [ + {'x': 1, 'y': 2}, + ], + 'collidedWords': [ + { + 'wordId': 'word-id', + 'position': 3, + 'character': 'b', + }, + ], }; final answer = Answer.fromJson(json); expect( answer, equals( - Answer(id: 'id', answer: 'answer', section: Point(1, 2)), + Answer( + id: 'id', + answer: 'answer', + sections: const [Point(1, 2)], + collidedWords: const [ + CollidedWord( + wordId: 'word-id', + position: 3, + character: 'b', + ), + ], + ), ), ); }); @@ -37,12 +77,26 @@ void main() { final firstAnswer = Answer( id: 'id', answer: 'answer', - section: Point(1, 2), + sections: const [Point(1, 2)], + collidedWords: const [ + CollidedWord( + wordId: 'word-id', + position: 3, + character: 'b', + ), + ], ); final secondAnswer = Answer( id: 'id', answer: 'answer', - section: Point(1, 2), + sections: const [Point(1, 2)], + collidedWords: const [ + CollidedWord( + wordId: 'word-id', + position: 3, + character: 'b', + ), + ], ); expect(firstAnswer, equals(secondAnswer)); }); diff --git a/api/test/routes/game/hint_test.dart b/api/test/routes/game/hint_test.dart index 1c0888e94..332a96c6b 100644 --- a/api/test/routes/game/hint_test.dart +++ b/api/test/routes/game/hint_test.dart @@ -88,7 +88,8 @@ void main() { return Answer( id: 'wordId', answer: 'answer', - section: Point(1, 1), + sections: [Point(1, 1)], + collidedWords: [], ); }, ); @@ -172,7 +173,8 @@ void main() { return Answer( id: 'wordId', answer: 'answer', - section: Point(1, 1), + sections: [Point(1, 1)], + collidedWords: [], ); }, ); @@ -214,7 +216,8 @@ void main() { return Answer( id: 'wordId', answer: 'answer', - section: Point(1, 1), + sections: [Point(1, 1)], + collidedWords: [], ); }, ); diff --git a/packages/board_generator/lib/create_sections.dart b/packages/board_generator/lib/create_sections.dart index ca083e092..0e6835e4e 100644 --- a/packages/board_generator/lib/create_sections.dart +++ b/packages/board_generator/lib/create_sections.dart @@ -102,40 +102,85 @@ void main(List args) async { for (var j = minSectionY; j < maxSectionY; j++) { final sectionX = i * sectionSize; final sectionY = j * sectionSize; - final sectionWords = wordsWithTopLeftOrigin - .where((word) { - return word.isAnyLetterInSection(sectionX, sectionY, sectionSize); - }) - .map( - (e) => e.copyWith( - position: Point(e.position.x - sectionX, e.position.y - sectionY), - ), - ) - .toList(); - - // TODO(ayad): add to [Answer] object the other words that are crossing it - - answers.addAll( - sectionWords.map((word) { - return Answer( - id: word.id, - answer: answersMap[word.id]!, - section: Point(i, j), - ); - }), - ); + final sectionWords = wordsWithTopLeftOrigin.where((word) { + return word.isAnyLetterInSection(sectionX, sectionY, sectionSize); + }); final section = BoardSection( id: '', position: Point(i, j), // remove this field from model (size) size: sectionSize, - words: sectionWords, + words: sectionWords + .map( + (e) => e.copyWith( + position: + Point(e.position.x - sectionX, e.position.y - sectionY), + ), + ) + .toList(), // remove this field from model (border words) borderWords: const [], ); sections.add(section); + + // Answers + for (final word in sectionWords) { + // The word is already added + if (answers.indexWhere((answer) => answer.answer == word.answer) > -1) { + continue; + } + + final allLetters = word.allLetters; + + final sectionsPoint = word.getSections(sectionX, sectionY, sectionSize); + + final allWordsSections = {} + ..addAll( + [ + for (final section in sections + .where((section) => sectionsPoint.contains(section.position))) + ...wordsWithTopLeftOrigin.where( + (word) { + return word.isAnyLetterInSection( + section.position.x * sectionSize, + section.position.y * sectionSize, + sectionSize, + ); + }, + ), + ], + ) + ..remove(word); + + final collidedWords = []; + + for (final word in allWordsSections) { + final collision = word + .copyWith(answer: answersMap[word.id]) + .getCollision(allLetters); + + if (collision != null) { + collidedWords.add( + CollidedWord( + character: collision.$2, + position: collision.$1, + wordId: word.id, + ), + ); + } + } + + answers.add( + Answer( + id: word.id, + answer: answersMap[word.id]!, + sections: sectionsPoint, + collidedWords: collidedWords, + ), + ); + } } } @@ -151,33 +196,68 @@ void main(List args) async { 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 + length - 1, position.y) - : (position.x, position.y + length - 1); - return endX >= sectionX && - endX < sectionX + sectionSize && - endY >= sectionY && - endY < sectionY + sectionSize; - } - +/// An extension on [Word] to check if it is in a section or collision +/// of characters of words. +extension WordExtension on Word { /// Returns true if any of its letters is in the section. + /// + /// The [sectionX] and [sectionY] value is the absolute position in the board. bool isAnyLetterInSection(int sectionX, int sectionY, int sectionSize) { return allLetters .any((e) => _isInSection(e.$1, e.$2, sectionX, sectionY, sectionSize)); } + /// Returns all the sections that the word crosses with the relative index of + /// the section based on the [sectionSize]. + /// + /// The [sectionX] and [sectionY] value is the absolute position in the board. + List> getSections(int sectionX, int sectionY, int sectionSize) { + final sections = >[]; + + switch (axis) { + case Axis.horizontal: + var i = 0; + + while (true) { + final x = sectionX + (sectionSize * i); + + if (isAnyLetterInSection(x, sectionY, sectionSize)) { + sections.add( + Point( + (x / sectionSize).floor(), + (sectionY / sectionSize).floor(), + ), + ); + } else { + break; + } + + i++; + } + case Axis.vertical: + var i = 0; + + while (true) { + final y = sectionY + (sectionSize * i); + + if (isAnyLetterInSection(sectionX, y, sectionSize)) { + sections.add( + Point( + (sectionX / sectionSize).floor(), + (y / sectionSize).floor(), + ), + ); + } else { + break; + } + + i++; + } + } + + return sections; + } + /// Returns all the letter positions of the word. List<(int, int)> get allLetters { return axis == Axis.horizontal @@ -190,10 +270,34 @@ extension SectionBelonging on Word { } /// Returns true if the point is in the section. + /// + /// The [x], [y], [sectionX] and [sectionY] value is the absolute + /// position in the board. bool _isInSection(int x, int y, int sectionX, int sectionY, int sectionSize) { return x >= sectionX && x < sectionX + sectionSize && y >= sectionY && y < sectionY + sectionSize; } + + /// Returns the index position of the collision. + /// If there are no collisions returns null. + (int, String)? getCollision(List<(int, int)> letters) { + switch (axis) { + case Axis.horizontal: + for (var i = 0; i < length; i++) { + if (letters.contains((position.x + i, position.y))) { + return (i, answer[i]); + } + } + case Axis.vertical: + for (var i = 0; i < length; i++) { + if (letters.contains((position.x, position.y + i))) { + return (i, answer[i]); + } + } + } + + return null; + } } diff --git a/packages/board_generator/test/src/create_sections_test.dart b/packages/board_generator/test/src/create_sections_test.dart new file mode 100644 index 000000000..55dc905f1 --- /dev/null +++ b/packages/board_generator/test/src/create_sections_test.dart @@ -0,0 +1,401 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:board_generator/create_sections.dart'; +import 'package:game_domain/game_domain.dart'; +import 'package:test/test.dart'; + +void main() { + group('WordExtension', () { + group('getSections', () { + group('horizontal', () { + test('finds only the first section starting on 0, 0', () { + final word = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HELLO', + ); + + expect( + word.getSections(0, 0, 5), + equals([Point(0, 0)]), + ); + }); + + test( + 'finds two sections with just by one character on the next section', + () { + final word = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HELLO', + ); + + expect( + word.getSections(0, 0, 4), + equals([Point(0, 0), Point(1, 0)]), + ); + }); + + test('finds three sections of small size starting on 0, 0', () { + final word = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HELLO', + ); + + expect( + word.getSections(0, 0, 2), + equals([Point(0, 0), Point(1, 0), Point(2, 0)]), + ); + }); + + test('finds two sections of small size starting on section 25, 0', () { + final word = Word( + id: 'id', + position: Point(100, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HELLO', + ); + + expect( + word.getSections(100, 0, 4), + equals([Point(25, 0), Point(26, 0)]), + ); + }); + }); + + group('vertical', () { + test('finds only the first section starting on 0, 0', () { + final word = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.vertical, + clue: '', + answer: 'HELLO', + ); + + expect( + word.getSections(0, 0, 5), + equals([Point(0, 0)]), + ); + }); + + test( + 'finds two sections with just by one character on the next section', + () { + final word = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.vertical, + clue: '', + answer: 'HELLO', + ); + + expect( + word.getSections(0, 0, 4), + equals([Point(0, 0), Point(0, 1)]), + ); + }); + + test('finds three sections of small size starting on 0, 0', () { + final word = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.vertical, + clue: '', + answer: 'HELLO', + ); + + expect( + word.getSections(0, 0, 2), + equals([Point(0, 0), Point(0, 1), Point(0, 2)]), + ); + }); + + test('finds two sections of small size starting on section 25, 0', () { + final word = Word( + id: 'id', + position: Point(0, 100), + axis: Axis.vertical, + clue: '', + answer: 'HELLO', + ); + + expect( + word.getSections(0, 100, 4), + equals([Point(0, 25), Point(0, 26)]), + ); + }); + }); + }); + + group('getCollision', () { + group('vertical', () { + test('finds the first character position', () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HELLO', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.vertical, + clue: '', + answer: 'HAPPY', + ); + + expect(word.getCollision(letters), equals((0, 'H'))); + }); + + test('finds the second character position', () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HAPPY', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(1, -1), + axis: Axis.vertical, + clue: '', + answer: 'SAD', + ); + + expect(word.getCollision(letters), equals((1, 'A'))); + }); + + test('finds the third character position', () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HELLO', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(3, -2), + axis: Axis.vertical, + clue: '', + answer: 'HALO', + ); + + expect(word.getCollision(letters), equals((2, 'L'))); + }); + + test('finds the fourth character position', () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HELLO', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(4, -3), + axis: Axis.vertical, + clue: '', + answer: 'HALO', + ); + + expect(word.getCollision(letters), equals((3, 'O'))); + }); + + test( + 'does not find collision when the character ' + 'is at the limit down', + () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HELLO', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(4, -4), + axis: Axis.vertical, + clue: '', + answer: 'HALO', + ); + + expect(word.getCollision(letters), isNull); + }, + ); + + test( + 'does not find collision when the character ' + 'is at the limit up', + () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HELLO', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(0, 1), + axis: Axis.vertical, + clue: '', + answer: 'HALO', + ); + + expect(word.getCollision(letters), isNull); + }, + ); + }); + + group('horizontal', () { + test('finds the first character position', () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.vertical, + clue: '', + answer: 'HELLO', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HAPPY', + ); + + expect(word.getCollision(letters), equals((0, 'H'))); + }); + + test('finds the second character position', () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.vertical, + clue: '', + answer: 'HAPPY', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(-1, 1), + axis: Axis.horizontal, + clue: '', + answer: 'SAD', + ); + + expect(word.getCollision(letters), equals((1, 'A'))); + }); + + test('finds the third character position', () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.vertical, + clue: '', + answer: 'HELLO', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(-2, 2), + axis: Axis.horizontal, + clue: '', + answer: 'HALO', + ); + + expect(word.getCollision(letters), equals((2, 'L'))); + }); + + test('finds the fourth character position', () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.vertical, + clue: '', + answer: 'HELLO', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(-3, 3), + axis: Axis.horizontal, + clue: '', + answer: 'HALO', + ); + + expect(word.getCollision(letters), equals((3, 'O'))); + }); + + test( + 'does not find collision when the character ' + 'is at the limit left', + () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.vertical, + clue: '', + answer: 'HELLO', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(-4, 3), + axis: Axis.horizontal, + clue: '', + answer: 'HALO', + ); + + expect(word.getCollision(letters), isNull); + }, + ); + + test( + 'does not find collision when the character ' + 'is at the limit left', + () { + final letters = Word( + id: 'id', + position: Point(0, 0), + axis: Axis.vertical, + clue: '', + answer: 'HELLO', + ).allLetters; + + final word = Word( + id: 'id', + position: Point(1, 0), + axis: Axis.horizontal, + clue: '', + answer: 'HALO', + ); + + expect(word.getCollision(letters), isNull); + }, + ); + }); + }); + }); +}