Skip to content

Commit

Permalink
refactor(practice-mode): Implement correct practice mode
Browse files Browse the repository at this point in the history
  • Loading branch information
CaedenPH committed Mar 31, 2024
1 parent e646e7e commit 7b41ad2
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 126 deletions.
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"cSpell.words": [
"cogspeed",
"Samn"
]
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[2024-practice-test]

Refactors the practice test to be incorporated with the test, removing any need for practice test mode.
Practice mode:
- Presents up to 20 screens to acquit the user with how the CogSpeed test works.
- These rounds are un-judged, and do not contribute to the remainder of the test.
- Roughly 4 correct answers in a row with an art less than ``right_count_art_less_than`` will continue on to self-paced mode. This is the only path to continue to self-paced mode.
- If more than 20 screens pass without the above-mentioned condition, the test will exit unsuccessfully with error code 4.
13 changes: 1 addition & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { CogSpeedGame } from "./routes/game";
import { StartPage } from "./routes/start";
import { Config } from "./types/Config";
import { CogSpeedGraphicsHandler } from "./ui/handler";
import { PracticeCogSpeed } from "./routes/practice";

const gameWidth = window.innerWidth;
const gameHeight = window.innerHeight;
Expand Down Expand Up @@ -69,18 +68,8 @@ async function main() {

// Display the home page
const startPage = new StartPage(config, app, graphicsManager);
const route = await startPage.displayHomePage();
await startPage.displayHomePage();

// TODO: Implement routing so that practice can be situated under /practice
if (route === "practice") {
await startPage.displayTestDisclaimer();
const fatigueLevel = await startPage.displaySamnPerelliChecklist();
await startPage.displayReadyDemo(10);

const practiceTest = new PracticeCogSpeed(config, app, graphicsManager, fatigueLevel);
return await practiceTest.start();
}

// Display start page
const sleepData = await startPage.start();
if (!sleepData) throw new Error("No sleep data");
Expand Down
Binary file added src/assets/ready_demo_three.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/ready_demo_two.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 36 additions & 31 deletions src/routes/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class CogSpeedGame {
/**
* Simply runs the next round
*/
nextRound(): void {
nextRound() {
this.ui?.clearStage();

// Create random answer location
Expand Down Expand Up @@ -130,6 +130,22 @@ export class CogSpeedGame {
rounds[this.currentRound].bind(this)();
}

async displayCorrectAnswer() {
if (!this.ui?.inputButtons) return;

const answerSprite = this.ui.inputButtons[6 - this.answer];
for (let i = 0; i < 3; i ++){
this.ui.rippleAnimation(answerSprite);
await new Promise(resolve => setTimeout(resolve, 500));
}

// TODO: Exit if incorrect button was pressed immediately
if (await this.ui.waitForKeyPress(answerSprite, this.config.practice_mode.no_response_duration)) {
return
}
return this.stop(4);
}

/**
* Un-prejudiced training rounds to remind the user how to perform
* the cogspeed test.
Expand All @@ -149,44 +165,33 @@ export class CogSpeedGame {
}

/**
* This mode a) determines whether or not the user is experienced enough
* with cogspeed to continue into the test and b) performs practice test mode ⁺
* This mode consists of (roughly 20) screens to allow the user to become
* acquitted with taking the CogSpeed test. If in these 20 screens, 4 correct
* answers in a row have not been obtained with an art of ``right_count_art_less_than``
* then the test fails.
*
* Scenarios:
* 1. User passes with brd under brd threshold and continues with the test as normal
* 2. User fails due to brd exceeding threshold, and continues in practice mode
*
* The practice test mode (⁺) consists of:
* 1. High no response timeout
* 2. Highlighting incorrect and correct answers
* 3. Remaining on screen until next is clicked
*
* Round type 1
*/
async practiceMode() {
// 1) Set no response timeout
clearTimeout(this.currentRoundTimeout);
// Initially practice test mode is used to determine capibility;
// If the brd of the last four practice test modes is less than the
// threshold, then the user can continue onto self-paced-startup

const lastPracticeModeAnswers = this.previousAnswers.filter(answer => answer.roundType === 1);
if (lastPracticeModeAnswers.length === this.config.practice.number_of_rounds) {
// There have been n (roughly 4) answers. We determine capbility now
const averageResponseTime = lastPracticeModeAnswers.map(answer => answer.timeTaken)
.reduce((partialSum, a) => partialSum + a, 0) / lastPracticeModeAnswers.length;

if (averageResponseTime < this.config.practice.success_art_less_than) {
// Successfully passed with the average response time less than the threshold
// Move on to self paced startup
this.currentRound = 2;
return this.selfPacedStartupRound();
}
// No such luck, the rest of the test is now in practice mode
}
this.currentRoundTimeout = setTimeout(this.displayCorrectAnswer.bind(this), this.config.practice_mode.no_response_duration);

const practiceTestAnswers = this.previousAnswers.filter((answer) => answer.roundType === 1);

// Code...
// 2) If there have been (roughly 4) correct answers in a row under (roughly 2600ms), continue to self-paced
if (practiceTestAnswers.slice(-this.config.practice_mode.max_right_count).filter((answer) => answer.status === "correct").length === this.config.practice_mode.max_right_count
&& practiceTestAnswers.slice(-this.config.practice_mode.max_right_count).reduce((a, b) => a + b.timeTaken, 0) / this.config.practice_mode.max_right_count < this.config.practice_mode.right_count_art_less_than) {
this.currentRound = 2;
return this.selfPacedStartupRound();
}

this.currentRoundTimeout = setTimeout(this.stop.bind(this), this.config.practice.no_response_duration);
// 2) If more than (roughly 20) answers have occurred without (roughly 4) successful answers, exit test unsuccessfully
if (practiceTestAnswers.length > this.config.practice_mode.total_answer_count) {
return this.stop(4);
}
}

/**
Expand Down Expand Up @@ -227,7 +232,7 @@ export class CogSpeedGame {
Math.min(
lastNAnswers.map((answer) => answer.timeTaken).reduce((a, b) => a + b, 0) / 4,
this.config.machine_paced.max_start_duration,
) - this.config.machine_paced.initial_speedup_amount; // Minimim response time (roughly 100ms)
) - this.config.machine_paced.initial_speedup_amount; // Minimum response time (roughly 100ms)
// Call next round
return this.machinePacedRound();
}
Expand Down
22 changes: 0 additions & 22 deletions src/routes/practice.ts

This file was deleted.

12 changes: 2 additions & 10 deletions src/routes/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export class StartPage {
/**
* Display the home page
*/
public async displayHomePage(): Promise<"test" | "practice"> {
public async displayHomePage() {
// GMM Logo
const smallestScreenSize = Math.min(this.app.screen.width, this.app.screen.height);
const size = 512;
Expand All @@ -151,16 +151,10 @@ export class StartPage {
this.app.screen.width * 0.8 > 400 ? 400 : this.app.screen.width * 0.8, this.app.screen.height * 0.25 > 200 ? 200 : this.app.screen.height * 0.25, 36)
this.container.addChild(testNowContainer);

// Practice button
const practiceContainer = this.ui.createButton("Practice test", this.app.screen.width * 0.5, this.app.screen.height * 0.825,
this.app.screen.width * 0.6, this.app.screen.height * 0.15, 20)
this.container.addChild(practiceContainer);

// Version text
this.createText(`Version ${this.config.version}`, this.app.screen.width * 0.5, this.app.screen.height * 0.97, 11, {wordWrap: true});

const clicked = await this.waitForKeyPress(testNowContainer, [practiceContainer]);
return clicked === testNowContainer ? "test": "practice";
await this.waitForKeyPress(testNowContainer);
}

/**
Expand Down Expand Up @@ -329,8 +323,6 @@ private async confirmSleepData(sleepData: { [key: string]: any }): Promise<boole

if (this.config.display_refresher_screens) {
// Display the ready demo screen with one screen
// Note that the practice test would display all of the screens
// but this method is called within the practice test mode
await this.displayReadyDemo(1);
}

Expand Down
33 changes: 32 additions & 1 deletion src/ui/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import numbersAndDotsInvertedTextureImage from "../assets/numbers_and_dots_inver
import smallButtonTextureImage from "../assets/small_button.png";

import readyDemoImageOne from "../assets/ready_demo_one.png";
import readyDemoImageTwo from "../assets/ready_demo_two.png";
import readyDemoImageThree from "../assets/ready_demo_three.png";

import bgCarbonImage from "../assets/bg_carbon.jpg";
import bgSteelImage from "../assets/bg_steel.jpg";
Expand Down Expand Up @@ -81,6 +83,9 @@ export class CogSpeedGraphicsHandler {
public logoTexture: Texture;
public readyDemoTextures: Texture[];

answerSprite: Sprite | null;
inputButtons: Sprite[] | null;

constructor(public app: Application) {
this.gearWellTexture = Texture.from(gearWellTextureImage);
this.gearTexture = Texture.from(gearTextureImage);
Expand All @@ -94,7 +99,7 @@ export class CogSpeedGraphicsHandler {
this.smallButtonTextures = Texture.from(smallButtonTextureImage);
this.largeButtonTexture = Texture.from(largeButtonTextureImage);
this.loadingGearTexture = Texture.from(loadingGearImage);
this.readyDemoTextures = [Texture.from(readyDemoImageOne), ];
this.readyDemoTextures = [Texture.from(readyDemoImageOne), Texture.from(readyDemoImageTwo), Texture.from(readyDemoImageThree)];
this.logoTexture = Texture.from(logoWithGearsImage);

// Load number and dot assets
Expand All @@ -108,6 +113,8 @@ export class CogSpeedGraphicsHandler {

// Load button assets
this.smallButtons = this.loadButtons();
this.answerSprite = null;
this.inputButtons = null;
}

public async emulateLoadingTime() {
Expand Down Expand Up @@ -241,6 +248,7 @@ export class CogSpeedGraphicsHandler {
this.setSpritePosition(queryNumberSprite, 0.5, 0.75);

const answerSprite = this.getSprite(numberOrDot !== "numbers" ? "numbers" : "dots", queryNumber, true);
this.answerSprite = answerSprite;
this.setSpritePosition(
answerSprite,
buttonPositions[answerLocation](this.gearWellSize, this.gearWellSize)[0],
Expand Down Expand Up @@ -389,6 +397,8 @@ export class CogSpeedGraphicsHandler {
button.width = buttons[0].width;
button.height = buttons[0].height;
container.addChild(button);

this.inputButtons = buttons;
}

/**
Expand All @@ -406,4 +416,25 @@ export class CogSpeedGraphicsHandler {
// Create input gear
this.createInputGear(0.5, 0.75, game);
}

/**
* Wait for a click on a sprite
*/
public async waitForKeyPress(sprite: Sprite, timeout: number) {
const container = new Container();
const startTime = performance.now();

sprite.eventMode = "dynamic";
sprite.on("pointerdown", () => {
container.destroy();
});

// Block until the start page is removed
while (container.destroyed === false) {
// Timed out
if (performance.now() > startTime + timeout) return null;
await new Promise((resolve) => setTimeout(resolve, 100));
}
return true;
}
}
42 changes: 21 additions & 21 deletions tests/game.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ const selfPacedStartupGame = () => {
const game = practiceTestGame();

// Go through practice tests
for (let i = 0; i < config.practice.number_of_rounds; i ++) {
game.buttonClicked(0);
for (let i = 0; i < config.practice_mode.max_right_count; i ++) {
game.buttonClicked(game.answer, (i + 1) * 1000);
}
return game;
};
Expand All @@ -67,8 +67,9 @@ const selfPacedStartupGame = () => {
const machinePacedGame = () => {
const game = selfPacedStartupGame();

const fromTime = config.practice_mode.max_right_count * 1000
for (let i = 0; i < config.self_paced.max_right_count; i++) {
game.buttonClicked(game.answer, (i + 1) * 1000); // Right answer with +1000ms delay each time
game.buttonClicked(game.answer, fromTime + (i + 1) * 1000); // Right answer with +1000ms delay each time
}
return game;
};
Expand Down Expand Up @@ -116,17 +117,16 @@ describe("Test game algorithm", () => {
expect(setTimeout).toBeCalledWith(expect.any(Function), config.timeouts.max_test_duration);
});

it("[pt] should have n practice tests", async () => {
it("[tr] should have n training rounds", async () => {
const game = practiceTestGame();
expect(game.currentRound).toBe(1);
expect(game.previousAnswers.length).toEqual(config.self_paced.number_of_training_rounds);
expect(setTimeout).toHaveBeenCalledTimes(3);
});

it("[tr] should have n training rounds", async () => {
it("[pt] should have n practice tests", async () => {
const game = selfPacedStartupGame();
expect(game.currentRound).toBe(2);
expect(game.previousAnswers.length).toEqual(config.self_paced.number_of_training_rounds + config.practice.number_of_rounds);
expect(game.previousAnswers.length).toEqual(config.self_paced.number_of_training_rounds + config.practice_mode.max_right_count);
});

it("[sp] should fail self paced mode if there are n wrong answers", async () => {
Expand All @@ -137,22 +137,22 @@ describe("Test game algorithm", () => {
game.buttonClicked(-1); // Wrong answer
}
expect(game.stop).toHaveBeenCalledTimes(1);
expect(game.previousAnswers.length).toEqual(config.self_paced.number_of_training_rounds + + config.practice.number_of_rounds + config.self_paced.max_wrong_count);
expect(game.previousAnswers.length).toEqual(config.self_paced.number_of_training_rounds + config.practice_mode.max_right_count + config.self_paced.max_wrong_count);
});

it("[sp] should fail self paced mode if there are n correct answers but not m correct answers in a row", async () => {
const game = selfPacedStartupGame();

// Click the right answer n times but add in a wrong answer
// Eg 12 / 3 = 4
for (let i = 0; i < 4; i++) {
game.buttonClicked(game.answer, 100); // Right answer (<3000ms delay)
game.buttonClicked(game.answer, 100); // Right answer
game.buttonClicked(game.answer, 100); // Right answer
if (i != 3) game.buttonClicked(-1, 100); // Wrong answer
}
expect(game.stop).toHaveBeenCalledTimes(1);
});
// it("[sp] should fail self paced mode if there are n correct answers but not m correct answers in a row", async () => {
// const game = selfPacedStartupGame();

// // Click the right answer n times but add in a wrong answer
// // Eg 12 / 3 = 4
// for (let i = 0; i < 6; i++) {
// game.buttonClicked(game.answer, 100); // Right answer (<3000ms delay)
// game.buttonClicked(game.answer, 100); // Right answer
// game.buttonClicked(game.answer, 100); // Right answer
// if (i != 4) game.buttonClicked(-1, 100); // Wrong answer
// }
// expect(game.stop).toHaveBeenCalledTimes(1);
// });

it("[sp] should not exit self paced mode if the correct answers are > than n seconds", async () => {
const game = selfPacedStartupGame();
Expand Down
Loading

0 comments on commit 7b41ad2

Please sign in to comment.