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
+
+
+
+
+
{{ stimuli.cardValues.length - cardsDrawn.length }} cards in the deck
+

+
+
+
+ [
+
+ {{ 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
}
}