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: update answer word endpoint #311

Merged
merged 7 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class CrosswordRepository {
final DbClient _dbClient;

static const _sectionsCollection = 'boardChunks';
static const _answersCollection = 'answers';
static const _boardInfoCollection = 'boardInfo';

/// Fetches all sections from the board.
Future<List<BoardSection>> listAllSections() async {
Expand Down Expand Up @@ -59,33 +61,44 @@ class CrosswordRepository {
);
}

/// Fetches a word answer by its id.
Future<String?> findAnswerById(String id) async {
final result = await _dbClient.getById(_answersCollection, 'id$id');

if (result != null) {
return result.data['answer'] as String;
}

return null;
}

/// Tries solving a word.
/// Returns true if succeeds and updates the word's solvedTimestamp
/// attribute.
Future<bool> answerWord(
int sectionX,
int sectionY,
jsgalarraga marked this conversation as resolved.
Show resolved Hide resolved
int wordX,
int wordY,
String wordId,
Mascots mascot,
AyadLaouissi marked this conversation as resolved.
Show resolved Hide resolved
String answer,
String userAnswer,
) async {
final section = await findSectionByPosition(sectionX, sectionY);

if (section == null) {
return false;
}
jsgalarraga marked this conversation as resolved.
Show resolved Hide resolved

final word = section.words.firstWhereOrNull(
(element) => element.position.x == wordX && element.position.y == wordY,
);
final word = section.words.firstWhereOrNull((e) => e.id == wordId);

if (word == null) {
return false;
}

if (answer == word.answer) {
final correctAnswer = await findAnswerById(wordId);

if (userAnswer.toLowerCase() == correctAnswer?.toLowerCase()) {
final solvedWord = word.copyWith(
answer: correctAnswer,
solvedTimestamp: clock.now().millisecondsSinceEpoch,
mascot: mascot,
);
Expand All @@ -97,4 +110,28 @@ class CrosswordRepository {
}
return false;
}

/// Adds one to the solved words count in the crossword.
Future<void> updateSolvedWordsCount() async {
final snapshot = await _dbClient.find(
_boardInfoCollection,
{
'type': 'solved_words_count',
},
);

final document = snapshot.first;
final solvedWordsCount = document.data['value'] as int;
final newValue = solvedWordsCount + 1;

await _dbClient.update(
_boardInfoCollection,
DbEntityRecord(
id: document.id,
data: {
'value': newValue,
},
),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// ignore_for_file: prefer_const_constructors
// ignore_for_file: prefer_const_literals_to_create_immutables

import 'package:clock/clock.dart';
import 'package:crossword_repository/crossword_repository.dart';
import 'package:db_client/db_client.dart';
Expand All @@ -15,6 +17,7 @@ void main() {
late DbClient dbClient;

const sectionsCollection = 'boardChunks';
const answersCollection = 'answers';

setUpAll(() {
registerFallbackValue(_MockDbEntityRecord());
Expand Down Expand Up @@ -160,29 +163,32 @@ void main() {
'position': {'x': 1, 'y': 1},
'size': 300,
'words': [
{
'id': '1',
...word.toJson(),
},
{'id': '1', ...word.toJson()},
],
'borderWords': const <dynamic>[],
},
);
when(
() => dbClient.find(
sectionsCollection,
{
'position.x': 1,
'position.y': 1,
},
{'position.x': 1, 'position.y': 1},
),
).thenAnswer((_) async => [record]);

when(
() => dbClient.update(
sectionsCollection,
any(that: isA<DbEntityRecord>()),
),
).thenAnswer((_) async {});

final answersRecord = _MockDbEntityRecord();
when(() => answersRecord.id).thenReturn('id1');
when(() => answersRecord.data).thenReturn({'answer': 'flutter'});
when(
() => dbClient.getById(answersCollection, 'id1'),
).thenAnswer((_) async => answersRecord);

repository = CrosswordRepository(dbClient: dbClient);
});

Expand All @@ -191,7 +197,7 @@ void main() {
final clock = Clock.fixed(time);
await withClock(clock, () async {
final valid =
await repository.answerWord(1, 1, 1, 1, Mascots.dino, 'flutter');
await repository.answerWord(1, 1, '1', Mascots.dino, 'flutter');
expect(valid, isTrue);
final captured = verify(
() => dbClient.update(
Expand Down Expand Up @@ -223,31 +229,67 @@ void main() {

test('answerWord returns false if answer is incorrect', () async {
final valid =
await repository.answerWord(1, 1, 1, 1, Mascots.dino, 'android');
await repository.answerWord(1, 1, '1', Mascots.dino, 'android');
expect(valid, isFalse);
});

test('answerWord returns false if section does not exist', () async {
when(
() => dbClient.find(
sectionsCollection,
{
'position.x': 0,
'position.y': 0,
},
{'position.x': 0, 'position.y': 0},
),
).thenAnswer((_) async => []);

final valid =
await repository.answerWord(0, 0, 1, 1, Mascots.dino, 'flutter');
await repository.answerWord(0, 0, '1', Mascots.dino, 'flutter');
expect(valid, isFalse);
});

test('answerWord returns false if word is not in section', () async {
final valid =
await repository.answerWord(1, 1, -1, -1, Mascots.dino, 'flutter');
await repository.answerWord(1, 1, 'fake', Mascots.dino, 'flutter');
expect(valid, isFalse);
});
});

group('updateSolvedWordsCount', () {
late CrosswordRepository repository;

setUp(() {
repository = CrosswordRepository(dbClient: dbClient);

when(
() => dbClient.update(
sectionsCollection,
any(),
),
).thenAnswer((_) async {});
});

test('updates the document in the database', () async {
final record = _MockDbEntityRecord();
when(() => record.id).thenReturn('id');
when(() => record.data).thenReturn({'value': 80});
when(
() => dbClient.find('boardInfo', {'type': 'solved_words_count'}),
).thenAnswer((_) async => [record]);
when(
() => dbClient.update('boardInfo', any()),
).thenAnswer((_) async {});

await repository.updateSolvedWordsCount();

verify(
() => dbClient.update(
'boardInfo',
DbEntityRecord(
id: 'id',
data: {'value': 81},
),
),
).called(1);
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,32 +58,35 @@ class LeaderboardRepository {
}

/// Updates the score for the provided user when it solves one word.
Future<void> updateScore(String userId) async {
Future<int> updateScore(String userId) async {
final playerData = await _dbClient.getById(_playersCollection, userId);

if (playerData == null) {
return;
return 0;
}

final player = Player.fromJson({
'id': userId,
...playerData.data,
});

final updatedPlayerData = increaseScore(player);
final points = getPointsForCorrectAnswer(player);
final updatedPlayerData = increaseScore(player, points);

return _dbClient.set(
await _dbClient.set(
_playersCollection,
DbEntityRecord(
id: updatedPlayerData.id,
data: updatedPlayerData.toJson(),
),
);

return points;
}

/// Increases the score for the provided score card.
/// Calculates the points for the provided player when it solves a word.
@visibleForTesting
Player increaseScore(Player player) {
int getPointsForCorrectAnswer(Player player) {
final streak = player.streak;

// Streak multiplier would be 1 for the first answer, 2 for the second,
Expand All @@ -94,8 +97,14 @@ class LeaderboardRepository {
const pointsPerWord = 10;
final points = streakMultiplier * pointsPerWord;

return points.round();
}

/// Increases the score for the provided player.
@visibleForTesting
Player increaseScore(Player player, int points) {
final updatedPlayerData = player.copyWith(
score: player.score + points.round(),
score: player.score + points,
streak: player.streak + 1,
);

Expand All @@ -104,17 +113,12 @@ class LeaderboardRepository {

/// Resets the streak for the provided user.
Future<void> resetStreak(String userId) async {
final playerData = await _dbClient.getById(_playersCollection, userId);
final player = await getPlayer(userId);

if (playerData == null) {
if (player == null) {
return;
}

final player = Player.fromJson({
'id': userId,
...playerData.data,
});

final updatedPlayerData = player.copyWith(streak: 0);

return _dbClient.set(
Expand All @@ -125,4 +129,18 @@ class LeaderboardRepository {
),
);
}

/// Retrieves the player for the provided user.
Future<Player?> getPlayer(String userId) async {
final playerData = await _dbClient.getById(_playersCollection, userId);

if (playerData == null) {
return null;
}

return Player.fromJson({
'id': userId,
...playerData.data,
});
}
}
Loading
Loading