diff --git a/src/app/models/ParticipantData.ts b/src/app/models/ParticipantData.ts index 77e5749..81e4a0b 100644 --- a/src/app/models/ParticipantData.ts +++ b/src/app/models/ParticipantData.ts @@ -204,6 +204,16 @@ export class IowaGamblingTaskData extends BaseParticipantData { feePaid: number; } +export class InformationTaskData extends BaseParticipantData { + roundNum: number; + trialScore: number; + cumulativeRoundScore: number; + cumulativeRoundLength: number; + trialResponseTime: number; + exploited: boolean; + expectedToExploit: boolean; +} + export class ParticipantData { userId: string; studyId: number; diff --git a/src/app/pages/tasks/task-playables/information-task/information-task.component.html b/src/app/pages/tasks/task-playables/information-task/information-task.component.html new file mode 100644 index 0000000..f3ceae5 --- /dev/null +++ b/src/app/pages/tasks/task-playables/information-task/information-task.component.html @@ -0,0 +1,45 @@ +
+
+
+
Click on the deck or an uncovered card
+
+
+
+
+ {{ card }} +
+
+
+
+
+
{{ stimuli.cardValues.length - cardsDrawn.length }} cards in the deck
+ Deck of cards image +
+
+
+ [ + + {{ value }}{{ i < valuesSelected.length - 1 ? ', ' : '' }} + + ] +
+
+
Turns Taken: {{ valuesSelected.length }}
+
+ Turns Left: {{ stimuli.cardValues.length - valuesSelected.length }} +
+
Total Points: {{ totalPoints }}
+
+
+
+
+
diff --git a/src/app/pages/tasks/task-playables/information-task/information-task.component.scss b/src/app/pages/tasks/task-playables/information-task/information-task.component.scss new file mode 100644 index 0000000..50e9113 --- /dev/null +++ b/src/app/pages/tasks/task-playables/information-task/information-task.component.scss @@ -0,0 +1,46 @@ +.card { + border-radius: 4px; + border: 4px solid black; + color: green; + padding: 1rem; + display: flex; + width: 100px; + height: 150px; + justify-content: center; + font-size: 3rem; + font-weight: bold; + cursor: pointer; + -webkit-user-select: none; /* Safari */ + -ms-user-select: none; /* IE 10 and IE 11 */ + user-select: none; /* Standard syntax */ + + &:hover { + box-shadow: 1px 1px 10px black; + } +} + +.card-container { + width: 10%; + height: 150px; + display: flex; + align-items: center; + justify-content: center; +} + +.cards-container { + width: 100%; + height: 400px; + display: flex; + flex-wrap: wrap; +} + +.deck { + &:hover { + opacity: 0.6; + cursor: pointer; + } +} + +.largest-card { + color: red !important; +} diff --git a/src/app/pages/tasks/task-playables/information-task/information-task.component.spec.ts b/src/app/pages/tasks/task-playables/information-task/information-task.component.spec.ts new file mode 100644 index 0000000..ca56fe1 --- /dev/null +++ b/src/app/pages/tasks/task-playables/information-task/information-task.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InformationTaskComponent } from './information-task.component'; + +describe('InformationTaskComponent', () => { + let component: InformationTaskComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ InformationTaskComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(InformationTaskComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/tasks/task-playables/information-task/information-task.component.ts b/src/app/pages/tasks/task-playables/information-task/information-task.component.ts new file mode 100644 index 0000000..d2f5cda --- /dev/null +++ b/src/app/pages/tasks/task-playables/information-task/information-task.component.ts @@ -0,0 +1,204 @@ +import { Component, HostListener, OnInit } from '@angular/core'; +import { AbstractBaseTaskComponent } from '../base-task'; +import { TimerService } from 'src/app/services/timer.service'; +import { LoaderService } from 'src/app/services/loader/loader.service'; +import { thisOrDefault, throwErrIfNotDefined } from 'src/app/common/commonMethods'; +import { TaskPlayerState } from '../task-player/task-player.component'; +import { StimuliProvidedType } from 'src/app/models/enums'; +import { ComponentName } from 'src/app/services/component-factory.service'; +import { DataGenerationService } from 'src/app/services/data-generation/data-generation.service'; +import { InformationTaskData } from 'src/app/models/ParticipantData'; +import { InformationTaskStimuliSet, InformationTaskStimulus } from 'src/app/services/data-generation/stimuli-models'; +import { TranslateService } from '@ngx-translate/core'; + +interface InformationTaskMetadata { + componentName: ComponentName; + componentConfig: { + numTrials: number; + roundNum: number; + isPractice: boolean; + stimuliConfig: { + type: StimuliProvidedType; + stimuli: InformationTaskStimuliSet; + }; + }; +} + +export enum InformationTaskCache { + TOTAL_SCORE = 'information-task-total-score', + OPTIMAL_SCORE = 'information-task-optimal-score', + STATUS_TEXT = 'information-task-status-text', +} + +@Component({ + selector: 'app-information-task', + templateUrl: './information-task.component.html', + styleUrls: ['./information-task.component.scss'], +}) +export class InformationTaskComponent extends AbstractBaseTaskComponent { + // config variables variables + private numTrials: number; + private roundNum: number; // determines which deck to use (1-6), translates into blockNum + private isPractice: boolean; + + // high level variables + taskData: InformationTaskData[]; + stimuli: InformationTaskStimuliSet; + currentStimuliIndex: number = 0; + cardsDrawn: number[]; + valuesSelected: number[]; + taskStarted: boolean = false; + roundStartTime: number; + trialNum: number = 0; + + // local state variables + + get currentStimulus(): InformationTaskStimulus { + return this.stimuli.cardValues[this.currentStimuliIndex]; + } + + get currentTrial(): InformationTaskData { + // will return null if taskData is not defined or if it has length of 0 + return this.taskData?.length > 0 ? this.taskData[this.taskData.length - 1] : null; + } + + get largestDrawnCardValue(): number { + return this.cardsDrawn.reduce((acc, curr) => (curr > acc ? curr : acc), 0); + } + + get totalPoints(): number { + return this.valuesSelected.reduce((acc, curr) => acc + curr, 0); + } + + // translation mapping + translationMapping = { + scoreStatusTextLower: { + en: 'You scored lower', + fr: '', + }, + scoreStatusTextEqual: { + en: 'You were equal!', + fr: '', + }, + scoreStatusTextHigher: { + en: 'You scored higher!', + fr: '', + }, + }; + + constructor( + protected timerService: TimerService, + protected dataGenService: DataGenerationService, + protected loaderService: LoaderService, + private translateService: TranslateService + ) { + super(loaderService); + } + + configure(metadata: InformationTaskMetadata, config?: TaskPlayerState) { + try { + this.userID = throwErrIfNotDefined(config.userID, 'no user ID defined'); + this.studyId = throwErrIfNotDefined(config.studyID, 'no study code defined'); + + this.numTrials = throwErrIfNotDefined(metadata.componentConfig.numTrials, 'num trials not defined'); + this.roundNum = throwErrIfNotDefined(metadata.componentConfig.roundNum, 'roundNum is not defined'); + this.isPractice = thisOrDefault(metadata.componentConfig.isPractice, false); + } catch (error) { + throw new Error('values not defined, cannot start study'); + } + + this.config = config; + if (metadata.componentConfig.stimuliConfig.type === StimuliProvidedType.HARDCODED) + this.stimuli = metadata.componentConfig.stimuliConfig.stimuli; + } + + start(): void { + console.log(this.stimuli); + // configure game + this.taskStarted = true; + + this.taskData = []; + this.cardsDrawn = []; + this.valuesSelected = []; + this.currentStimuliIndex = 0; + this.trialNum = 0; + + this.roundStartTime = Date.now(); + super.start(); + } + + beginRound() { + this.timerService.clearTimer(); + this.timerService.startTimer(); + } + + handleRoundInteraction(cardType: 'newCard' | 'existingCard', existingCardVal?: number) { + if (!this.taskStarted) return; + + const newCardVal = this.currentStimulus; + + let cardValue: number; + if (cardType === 'newCard') { + this.cardsDrawn.push(newCardVal.cardValue); + cardValue = newCardVal.cardValue; + } else { + cardValue = existingCardVal; + } + + this.valuesSelected.push(cardValue); + + this.taskData.push({ + userID: this.userID, + studyId: this.studyId, + submitted: this.timerService.getCurrentTimestamp(), + isPractice: this.isPractice, + trial: ++this.trialNum, + roundNum: this.roundNum, + trialScore: cardValue, + cumulativeRoundLength: Date.now() - this.roundStartTime, + cumulativeRoundScore: + this.taskData.length <= 0 + ? cardValue + : this.taskData[this.taskData.length - 1].cumulativeRoundScore + cardValue, + exploited: cardType === 'existingCard', + expectedToExploit: newCardVal.expectedToExploit, + trialResponseTime: this.timerService.stopTimerAndGetTime(), + }); + super.handleRoundInteraction(null); + } + + async completeRound() { + super.completeRound(); + } + + async decideToRepeat(): Promise { + if (this.trialNum >= this.numTrials) { + this.taskStarted = false; + const totalScore = this.taskData.reduce((acc, currVal) => { + return acc + currVal.trialScore; + }, 0); + + const optimalScore = this.stimuli.optimalScore; + + console.log({ totalScore, optimalScore }); + + const statusText = + totalScore < optimalScore + ? this.translationMapping.scoreStatusTextLower[this.translateService.currentLang] + : totalScore === optimalScore + ? this.translationMapping.scoreStatusTextEqual[this.translateService.currentLang] + : this.translationMapping.scoreStatusTextHigher[this.translateService.currentLang]; + + // this will replace the previous round + this.config.setCacheValue(InformationTaskCache.TOTAL_SCORE, totalScore); + this.config.setCacheValue(InformationTaskCache.OPTIMAL_SCORE, optimalScore); + this.config.setCacheValue(InformationTaskCache.STATUS_TEXT, statusText); + + super.decideToRepeat(); + } else { + this.currentStimuliIndex++; + this.beginRound(); + return; + } + } +} diff --git a/src/app/pages/tasks/task.module.ts b/src/app/pages/tasks/task.module.ts index b228eff..f4e4c5a 100644 --- a/src/app/pages/tasks/task.module.ts +++ b/src/app/pages/tasks/task.module.ts @@ -41,6 +41,7 @@ import { BlankComponent } from './blank/blank.component'; import { ProbabilisticLearningTaskComponent } from './task-playables/probabilistic-learning-task/probabilistic-learning-task.component'; import { OptionDisplayComponent } from './shared/option-display/option-display.component'; import { IowaGamblingTaskComponent } from './task-playables/iowa-gambling-task/iowa-gambling-task.component'; +import { InformationTaskComponent } from './task-playables/information-task/information-task.component'; @NgModule({ declarations: [ @@ -64,6 +65,7 @@ import { IowaGamblingTaskComponent } from './task-playables/iowa-gambling-task/i TaskPlayerComponent, TaskSwitchingComponent, TrailMakingComponent, + InformationTaskComponent, EmbeddedPageComponent, diff --git a/src/app/services/component-factory.service.ts b/src/app/services/component-factory.service.ts index 1b832d2..35411d1 100644 --- a/src/app/services/component-factory.service.ts +++ b/src/app/services/component-factory.service.ts @@ -20,6 +20,7 @@ import { EmbeddedPageComponent } from '../pages/tasks/task-playables/embedded-pa import { InfoDisplayComponent } from '../pages/tasks/task-playables/info-display/info-display.component'; import { ProbabilisticLearningTaskComponent } from '../pages/tasks/task-playables/probabilistic-learning-task/probabilistic-learning-task.component'; import { IowaGamblingTaskComponent } from '../pages/tasks/task-playables/iowa-gambling-task/iowa-gambling-task.component'; +import { InformationTaskComponent } from '../pages/tasks/task-playables/information-task/information-task.component'; export enum ComponentName { // Generic components @@ -42,6 +43,7 @@ export enum ComponentName { FACE_NAME_ASSOCIATION_COMPONENT = 'FACENAMEASSOCIATIONCOMPONENT', PLT_COMPONENT = 'PLTCOMPONENT', IOWA_GAMBLING_COMPONENT = 'IOWAGAMBLINGCOMPONENT', + INFORMATION_TASK_COMPONENT = 'INFORMATIONTASKCOMPONENT', // Special Components EMBEDDED_PAGE_COMPONENT = 'EMBEDDEDPAGECOMPONENT', @@ -72,6 +74,7 @@ const ComponentMap = { [ComponentName.INFO_DISPLAY_COMPONENT]: InfoDisplayComponent, [ComponentName.PLT_COMPONENT]: ProbabilisticLearningTaskComponent, [ComponentName.IOWA_GAMBLING_COMPONENT]: IowaGamblingTaskComponent, + [ComponentName.INFORMATION_TASK_COMPONENT]: InformationTaskComponent, }; @Injectable({ diff --git a/src/app/services/data-generation/stimuli-models.ts b/src/app/services/data-generation/stimuli-models.ts index 484b42d..761a9c8 100644 --- a/src/app/services/data-generation/stimuli-models.ts +++ b/src/app/services/data-generation/stimuli-models.ts @@ -164,3 +164,13 @@ export class IowaGamblingTaskStimulus { moneyWon: number; feePaid: number; } + +export class InformationTaskStimuliSet { + optimalScore: number; + cardValues: InformationTaskStimulus[]; +} + +export class InformationTaskStimulus { + cardValue: number; + expectedToExploit: boolean; +} diff --git a/src/assets/images/stimuli/informationtask/deckOfCards.png b/src/assets/images/stimuli/informationtask/deckOfCards.png new file mode 100644 index 0000000..6206013 Binary files /dev/null and b/src/assets/images/stimuli/informationtask/deckOfCards.png differ diff --git a/tsconfig.base.json b/tsconfig.base.json index 63e8b74..9fff3ad 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -16,6 +16,7 @@ }, "angularCompilerOptions": { "fullTemplateTypeCheck": true, - "strictInjectionParameters": true + "strictInjectionParameters": true, + "strictTempaltes": true } }