Skip to content

Commit

Permalink
Added: Add score feedback display options to MatchingPairsGame (#724)
Browse files Browse the repository at this point in the history
* feat: Add score feedback display options to Playback and MatchingPairsGame

* feat: Refactor MatchingPairs component and add scoreFeedbackDisplay prop

* story: Add SCORE_FEEDBACK_DISPLAY option to MatchingPairs component

* test: Add tests for score feedback display options in MatchingPairs component

* lint: Add blank line for readability in Playback class

* refactor: Refactor score display variable names

* fix(docs): Fix scoreFeedbackDisplay casing in MatchingPairs component

* fix: Correct score_feedback_display's default value

* refactor: Remove score feedback display option from Playback action class

* fix: Fix score feedback display in MatchingPairs class

* docs: Add sections and score feedback display to MatchingPairs class documentation

* story: Add WithShowAnimation story to MatchingPairs component

* style: Make the histogram block to be square always
  • Loading branch information
drikusroor authored Jan 23, 2024
1 parent a35e9b2 commit de2b6db
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 38 deletions.
9 changes: 6 additions & 3 deletions backend/experiment/actions/playback.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ def __init__(self,
mute=False,
timeout_after_playback=None,
stop_audio_after=None,
resume_play=False):
resume_play=False,
):
self.sections = [{'id': s.id, 'url': s.absolute_url(), 'group': s.group}
for s in sections]
if str(sections[0].filename).startswith('http'):
Expand Down Expand Up @@ -127,12 +128,14 @@ class MatchingPairs(Multiplayer):
'''
This is a special case of multiplayer:
play buttons are represented as cards
- sections: a list of sections (in many cases, will only contain *one* section)
- score_feedback_display: how to display the score feedback (large-top, small-bottom-right, hidden)
'''

def __init__(self, sections: List[Dict], display_score: str = 'large_top', **kwargs):
def __init__(self, sections: List[Dict], score_feedback_display: str = 'large-top', **kwargs):
super().__init__(sections, **kwargs)
self.ID = TYPE_MATCHINGPAIRS
self.display_score = display_score
self.score_feedback_display = score_feedback_display


class VisualMatchingPairs(MatchingPairs):
Expand Down
6 changes: 3 additions & 3 deletions backend/experiment/rules/matching_pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class MatchingPairsGame(Base):
ID = 'MATCHING_PAIRS'
num_pairs = 8
show_animation = True
display_score = 'top'
score_feedback_display = 'large-top'
histogram_bars = 5
contact_email = 'aml.tunetwins@gmail.com'

Expand Down Expand Up @@ -111,14 +111,14 @@ def get_matching_pairs_trial(self, session):
sections=player_sections,
stop_audio_after=5,
show_animation=self.show_animation,
display_score=self.display_score
score_feedback_display=self.score_feedback_display,
)
trial = Trial(
title='Tune twins',
playback=playback,
feedback_form=None,
result_id=prepare_result('matching_pairs', session),
config={'show_continue_button': False}
config={ 'show_continue_button': False }
)
return trial

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/Histogram/Histogram.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
.aha__histogram {
display: flex;
aspect-ratio: 1/1;
transform: scaleY(-1);
margin-bottom: 5px;
margin-top: -10px;

div {
border-radius: 1.5px;
background: white;
Expand Down
61 changes: 42 additions & 19 deletions frontend/src/components/Playback/MatchingPairs.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import React, {useRef, useState} from "react";
import React, { useRef, useState } from "react";
import classNames from "classnames";

import PlayCard from "../PlayButton/PlayCard";

export const SCORE_FEEDBACK_DISPLAY = {
SMALL_BOTTOM_RIGHT: 'small-bottom-right',
LARGE_TOP: 'large-top',
HIDDEN: 'hidden',
}

const MatchingPairs = ({
playSection,
sections,
playerIndex,
stopAudioAfter,
showAnimation,
displayScore,
finishedPlaying,
scoreFeedbackDisplay = SCORE_FEEDBACK_DISPLAY.LARGE_TOP, // 'large-top' (default) | 'small-bottom-right' | 'hidden'
submitResult,
}) => {

const xPosition = useRef(-1);
const yPosition = useRef(-1);
const score = useRef(undefined);
Expand Down Expand Up @@ -44,7 +51,7 @@ const MatchingPairs = ({
}

const formatTime = (time) => {
return time/1000;
return time / 1000;
}

// Show (animated) feedback after second click on second card or finished playing
Expand All @@ -54,10 +61,10 @@ const MatchingPairs = ({
// Check if this turn has finished
if (turnedCards.length === 2) {
// update total score & display current score
setTotal(total+score.current);
setTotal(total + score.current);
setMessage(setScoreMessage(score.current));
setMessage(setScoreMessage(score.current));
// show end of turn animations
// show end of turn animations if enabled
if (showAnimation) {
switch (score.current) {
case 10:
Expand Down Expand Up @@ -160,28 +167,19 @@ const MatchingPairs = ({
}

if (end) {
submitResult({score: total, moves: resultBuffer.current});
submitResult({ score: total, moves: resultBuffer.current });
}

return (
<div className="aha__matching-pairs">
<div className="row justify-content-around">
<div className="col-6 align-self-start">
<div dangerouslySetInnerHTML={{ __html: message }}
className={classNames("matching-pairs__feedback", { nomessage: displayScore === 'hidden' }, {fbnomatch: score.current === 0}, {fblucky: score.current === 10}, {fbmemory: score.current === 20}, {fbmisremembered: score.current === -10})}

/>
</div>
<div className="col-6 align-self-end">
<div className={classNames("matching-pairs__score", { noscore: displayScore === 'hidden' } )}>Score: <br />{total}</div>
</div>
</div>

{scoreFeedbackDisplay !== SCORE_FEEDBACK_DISPLAY.HIDDEN && <ScoreFeedback message={message} score={score} total={total} scoreFeedbackDisplay={scoreFeedbackDisplay} />}

<div className={classNames("playing-board", columnCount === 3 && "playing-board--three-columns")}>
{Object.keys(sections).map((index) => (
<PlayCard
key={index}
onClick={()=> {
onClick={() => {
playSection(index);
checkMatchingPairs(index);
}}
Expand All @@ -195,10 +193,35 @@ const MatchingPairs = ({
)
)}
</div>
<div className={classNames("turnscore", { noturnscore: displayScore === 'hidden'})}>{turnFeedback}</div>
</div>
)
}

const ScoreFeedback = ({
message,
scoreFeedbackDisplay = SCORE_FEEDBACK_DISPLAY.LARGE_TOP,
score,
total,
}) => {
return (
<div className={
classNames(
"matching-pairs__score-feedback row justify-content-around",
{ "matching-pairs__score-feedback--small-bottom-right": scoreFeedbackDisplay === SCORE_FEEDBACK_DISPLAY.SMALL_BOTTOM_RIGHT },
)}
>
<div className="col-6 align-self-start">
<div dangerouslySetInnerHTML={{ __html: message }}
className={classNames("matching-pairs__feedback", { fbnomatch: score.current === 0 }, { fblucky: score.current === 10 }, { fbmemory: score.current === 20 }, { fbmisremembered: score.current === -10 })}

/>
</div>
<div className="col-6 align-self-end">
<div className="matching-pairs__score">Score: <br />{total}</div>
</div>
</div>
)
}

export default MatchingPairs;

27 changes: 20 additions & 7 deletions frontend/src/components/Playback/MatchingPairs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
}

.aha__matching-pairs {
position: absolute;
left: 50%;
transform: translateX(-50%);
margin: 0px auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

@media (min-aspect-ratio: 1/1) {
Expand Down Expand Up @@ -45,15 +45,29 @@

.playing-board {
display: inline-grid;
max-width: 100%;
}

.matching-pairs__score-feedback {

font-weight: 700;

&--small-bottom-right {
position: fixed;
justify-content: flex-end;
bottom: 0;
right: 0;
text-align: right;
font-size: 0.5rem;
backface-visibility: hidden;
font-weight: normal;
}
}

.matching-pairs__feedback, .matching-pairs__score {
font-family: 'Rajdhani', sans-serif;
line-height: 1;
text-align: center;
padding-bottom: .7rem;
font-weight: 700;
font-size: 95%;
width: auto;

Expand All @@ -63,7 +77,6 @@
}

.matching-pairs__feedback {
float: left;
color: white;

&.fbnomatch {
Expand Down
26 changes: 25 additions & 1 deletion frontend/src/components/Playback/MatchingPairs.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react';
import MatchingPairs from './MatchingPairs';
import MatchingPairs, { SCORE_FEEDBACK_DISPLAY } from './MatchingPairs';

jest.mock("../PlayButton/PlayCard", () => (props) => (
<div data-testid="play-card" {...props} />
Expand Down Expand Up @@ -28,4 +28,28 @@ describe('MatchingPairs Component', () => {
const { container } = render(<MatchingPairs {...baseProps} sections={sections} />);
expect(container.querySelector('.playing-board--two-columns')).not.toBeInTheDocument();
});

it('displays score feedback when scoreFeedbackDisplay is not HIDDEN', () => {
const sections = new Array(6).fill({}).map((_, index) => ({ id: index }));
const { container } = render(<MatchingPairs {...baseProps} sections={sections} scoreFeedbackDisplay={SCORE_FEEDBACK_DISPLAY.LARGE_TOP} />);
expect(container.querySelector('.matching-pairs__score-feedback')).toBeInTheDocument();
});

it('does not display score feedback when scoreFeedbackDisplay is HIDDEN', () => {
const sections = new Array(6).fill({}).map((_, index) => ({ id: index }));
const { container } = render(<MatchingPairs {...baseProps} sections={sections} scoreFeedbackDisplay={SCORE_FEEDBACK_DISPLAY.HIDDEN} />);
expect(container.querySelector('.matching-pairs__score-feedback')).not.toBeInTheDocument();
});

it('displays score feedback on the top when scoreFeedbackDisplay is LARGE_TOP', () => {
const sections = new Array(6).fill({}).map((_, index) => ({ id: index }));
const { container } = render(<MatchingPairs {...baseProps} sections={sections} scoreFeedbackDisplay={SCORE_FEEDBACK_DISPLAY.LARGE_TOP} />);
expect(container.querySelector('.matching-pairs__score-feedback--small-bottom-right')).not.toBeInTheDocument();
});

it('displays score feedback on the bottom right when scoreFeedbackDisplay is SMALL_BOTTOM_RIGHT', () => {
const sections = new Array(6).fill({}).map((_, index) => ({ id: index }));
const { container } = render(<MatchingPairs {...baseProps} sections={sections} scoreFeedbackDisplay={SCORE_FEEDBACK_DISPLAY.SMALL_BOTTOM_RIGHT} />);
expect(container.querySelector('.matching-pairs__score-feedback--small-bottom-right')).toBeInTheDocument();
});
});
3 changes: 2 additions & 1 deletion frontend/src/components/Playback/Playback.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,14 @@ const Playback = ({
{...attrs}
stopAudioAfter={playbackArgs.stop_audio_after}
showAnimation={playbackArgs.show_animation}
displayScore={playbackArgs.display_score}
scoreFeedbackDisplay={playbackArgs.score_feedback_display}
/>
);
case VISUALMATCHINGPAIRS:
return (
<VisualMatchingPairs
{...attrs}
scoreFeedbackDisplay={playbackArgs.score_feedback_display}
/>
);
default:
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/Trial/Trial.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ const Trial = ({
return;
}, [config, playback, makeResult]);


return (
<div role="presentation" className={classNames("aha__trial", config.style)}>
{playback && (
Expand Down
47 changes: 44 additions & 3 deletions frontend/src/stories/MatchingPairs.stories.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import MatchingPairs from '../components/Playback/MatchingPairs';
import MatchingPairs, { SCORE_FEEDBACK_DISPLAY } from '../components/Playback/MatchingPairs';

import audio from './assets/audio.wav';

Expand Down Expand Up @@ -149,8 +149,7 @@ export const WithThreeColumns = {
memory: false,
},
],
},
),
}),
decorators: [
(Story) => (
<div id="root" style={{ width: '100%', height: '100%', backgroundColor: '#ddd', padding: '1rem' }}>
Expand All @@ -166,3 +165,45 @@ export const WithThreeColumns = {
},
},
};

export const WithSmallBottomRightScoreFeedback = {
args: {
...getDefaultArgs(),
scoreFeedbackDisplay: SCORE_FEEDBACK_DISPLAY.SMALL_BOTTOM_RIGHT
},
decorators: [
(Story) => (
<div id="root" style={{ width: '100%', height: '100%', backgroundColor: '#ddd', padding: '1rem' }}>
<Story />
</div>
),
],
parameters: {
docs: {
description: {
component: 'This story shows the component with the default props.',
},
},
},
};

export const WithShowAnimation = {
args: {
...getDefaultArgs(),
showAnimation: true,
},
decorators: [
(Story) => (
<div id="root" style={{ width: '100%', height: '100%', backgroundColor: '#ddd', padding: '1rem' }}>
<Story />
</div>
),
],
parameters: {
docs: {
description: {
component: 'This story shows the component with the default props.',
},
},
},
};

0 comments on commit de2b6db

Please sign in to comment.