Skip to content

Commit

Permalink
Add recognition of another player’s win
Browse files Browse the repository at this point in the history
  • Loading branch information
backspace committed Nov 14, 2024
1 parent acc8b80 commit 4ef55c1
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 21 deletions.
8 changes: 7 additions & 1 deletion registrations/lib/registrations/waydowntown.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ defmodule Registrations.Waydowntown do
|> Repo.preload(run_preloads())
|> Repo.preload(participations: [run: run_preloads()])
|> Repo.preload([participations: [:user]], prefix: "public")
|> Repo.preload([submissions: [:creator]], prefix: "public")
end

def create_run(current_user, attrs \\ %{}, specification_filter \\ nil) do
Expand Down Expand Up @@ -312,7 +313,12 @@ defmodule Registrations.Waydowntown do
|> Repo.update()
end

def get_submission!(id), do: Submission |> Repo.get!(id) |> Repo.preload(submission_preloads())
def get_submission!(id),
do:
Submission
|> Repo.get!(id)
|> Repo.preload(submission_preloads())
|> Repo.preload([:creator, run: [submissions: [:creator]]], prefix: "public")

def create_submission(conn, %{"submission" => submission_text, "run_id" => run_id} = params) do
current_user_id = conn.assigns[:current_user].id
Expand Down
2 changes: 1 addition & 1 deletion registrations/lib/registrations_web/views/run_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule RegistrationsWeb.RunView do
alias Registrations.Waydowntown

def fields do
[:complete, :correct_submissions, :total_answers, :task_description, :started_at, :competitors]
[:complete, :correct_submissions, :total_answers, :task_description, :started_at, :competitors, :winner_submission_id]
end

def hidden(run) do
Expand Down
6 changes: 5 additions & 1 deletion registrations/lib/registrations_web/views/submission_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ defmodule RegistrationsWeb.SubmissionView do
end

def relationships do
[answer: {RegistrationsWeb.AnswerView, :include}, run: {RegistrationsWeb.RunView, :include}]
[
answer: {RegistrationsWeb.AnswerView, :include},
run: {RegistrationsWeb.RunView, :include},
creator: {RegistrationsWeb.JSONAPI.UserView, :include}
]
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ defmodule RegistrationsWeb.SubmissionControllerTest do
}
end

test "creates correct submission", %{conn: conn, run: run, answer: answer} do
test "creates correct submission", %{conn: conn, user: user, run: run, answer: answer} do
conn =
conn
|> setup_conn()
|> setup_conn(user)
|> post(
Routes.submission_path(conn, :create),
%{
Expand Down Expand Up @@ -147,6 +147,9 @@ defmodule RegistrationsWeb.SubmissionControllerTest do
sideloaded_answer = Enum.find(included, &(&1["type"] == "answers"))
assert sideloaded_answer["id"] == answer.id

sideloaded_user = Enum.find(included, &(&1["type"] == "users"))
assert sideloaded_user["id"] == user.id

assert_broadcast "run_update", payload

assert payload.data.type == "runs"
Expand Down
54 changes: 39 additions & 15 deletions waydowntown_app/lib/mixins/run_state_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ import 'package:flutter/material.dart';
import 'package:phoenix_socket/phoenix_socket.dart';
import 'package:waydowntown/app.dart';
import 'package:waydowntown/models/run.dart';
import 'package:waydowntown/widgets/completion_animation.dart';
import 'package:waydowntown/models/submission.dart';
import 'package:waydowntown/services/user_service.dart';
import 'package:waydowntown/widgets/losing_animation.dart';
import 'package:waydowntown/widgets/winning_animation.dart';

enum GameState {
inProgress,
won,
lost,
}

mixin RunStateMixin<T extends StatefulWidget> on State<T> {
late Run currentRun;
bool isGameComplete = false;
GameState gameState = GameState.inProgress;

Dio get dio;
Run get initialRun;
Expand All @@ -20,8 +29,14 @@ mixin RunStateMixin<T extends StatefulWidget> on State<T> {
currentRun = initialRun;
}

bool get isGameComplete => gameState != GameState.inProgress;

void _showCompletionAnimation() {
CompletionAnimation.show(context);
if (gameState == GameState.won) {
WinningAnimation.show(context);
} else {
LosingAnimation.show(context);
}
}

Future<bool> submitSubmission(String submission, {String? answerId}) async {
Expand Down Expand Up @@ -60,12 +75,9 @@ mixin RunStateMixin<T extends StatefulWidget> on State<T> {
currentRun = Run.fromJson(
{'data': runData, 'included': response.data['included']},
existingSpecification: currentRun.specification);

if (currentRun.isComplete) {
isGameComplete = true;
_showCompletionAnimation();
}
});

await checkForCompletion();
}
}

Expand All @@ -76,23 +88,35 @@ mixin RunStateMixin<T extends StatefulWidget> on State<T> {
}
}

void initializeChannel(PhoenixChannel gameChannel) {
void initializeChannel(PhoenixChannel gameChannel) async {
channel = gameChannel;
channel!.messages.listen((message) {
channel!.messages.listen((message) async {
if (message.event == const PhoenixChannelEvent.custom('run_update')) {
setState(() {
currentRun = Run.fromJson(message.payload!,
existingSpecification: currentRun.specification);

if (currentRun.isComplete && !isGameComplete) {
isGameComplete = true;
_showCompletionAnimation();
}
});

await checkForCompletion();
}
});
}

Future<void> checkForCompletion() async {
if (currentRun.isComplete &&
gameState == GameState.inProgress &&
currentRun.winnerSubmissionId != null) {
final String? winnerSubmissionId = currentRun.winnerSubmissionId;
final Submission winningSubmission = currentRun.submissions
.firstWhere((submission) => submission.id == winnerSubmissionId);
final bool isUserSubmission =
winningSubmission.creatorId == await UserService.getUserId();

gameState = isUserSubmission ? GameState.won : GameState.lost;
_showCompletionAnimation();
}
}

@override
void dispose() {
channel?.leave();
Expand Down
26 changes: 26 additions & 0 deletions waydowntown_app/lib/models/run.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:waydowntown/models/participation.dart';
import 'package:waydowntown/models/specification.dart';
import 'package:waydowntown/models/submission.dart';

class Run {
final String id;
Expand All @@ -10,6 +11,8 @@ class Run {
final String? taskDescription;
final bool isComplete;
final List<Participation> participations;
final String? winnerSubmissionId;
final List<Submission> submissions;

Run({
required this.id,
Expand All @@ -20,6 +23,8 @@ class Run {
this.taskDescription,
this.isComplete = false,
required this.participations,
this.winnerSubmissionId,
required this.submissions,
});

factory Run.fromJson(Map<String, dynamic> json,
Expand Down Expand Up @@ -71,6 +76,25 @@ class Run {
.toList();
}

List<Submission> submissions = [];
if (included != null &&
data['relationships'] != null &&
data['relationships']['submissions'] != null) {
final submissionsData =
data['relationships']['submissions']['data'] as List;

submissions = submissionsData
.map((submissionData) => Submission.fromJson(included.firstWhere(
(item) =>
item['type'] == 'submissions' &&
item['id'] == submissionData['id'] &&
// FIXME serialisation crisis
item['relationships']['creator'] != null,
orElse: () => <String, Object>{},
)))
.toList();
}

return Run(
id: data['id'],
specification: specification ??
Expand All @@ -83,6 +107,8 @@ class Run {
: null,
isComplete: data['attributes']['complete'] ?? false,
participations: participations,
submissions: submissions,
winnerSubmissionId: data['attributes']['winner_submission_id'],
);
}
}
27 changes: 27 additions & 0 deletions waydowntown_app/lib/models/submission.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Submission {
final String id;
final String submission;
final bool correct;
final DateTime insertedAt;
final String? creatorId;

Submission({
required this.id,
required this.submission,
required this.correct,
required this.insertedAt,
this.creatorId,
});

factory Submission.fromJson(Map<String, dynamic> json) {
print('submission json');
print(json);
return Submission(
id: json['id'],
submission: json['attributes']['submission'],
correct: json['attributes']['correct'],
insertedAt: DateTime.parse(json['attributes']['inserted_at']),
creatorId: json['relationships']['creator']['data']['id'],
);
}
}
84 changes: 84 additions & 0 deletions waydowntown_app/lib/widgets/losing_animation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';

class LosingAnimation extends StatefulWidget {
const LosingAnimation({super.key});

static void show(BuildContext context) {
showDialog(
context: context,
barrierDismissible: true,
barrierColor: Colors.transparent,
builder: (context) => const LosingAnimation(),
);
}

@override
State<LosingAnimation> createState() => _LosingAnimationState();
}

class _LosingAnimationState extends State<LosingAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;

@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);

_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));

_controller.forward();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: Container(
color: Colors.black,
child: Stack(
children: [
const Center(
child: Text(
'You lost!',
style: TextStyle(
color: Colors.white,
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
),
Positioned(
top: 40,
right: 16,
child: IconButton(
icon: const Icon(
Icons.close,
color: Colors.white,
size: 32,
),
onPressed: () => Navigator.of(context).pop(),
),
),
],
),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_confetti/flutter_confetti.dart';

class CompletionAnimation {
class WinningAnimation {
static void show(BuildContext context) {
const options = ConfettiOptions(
spread: 360,
Expand Down

0 comments on commit 4ef55c1

Please sign in to comment.