feat(opord): scale tiers to 10/15/20 questions with expanded briefings
This commit is contained in:
parent
987250b60a
commit
0996b9b7c2
4 changed files with 188 additions and 20 deletions
|
|
@ -13,9 +13,9 @@ import { OpordGame } from "./OpordGame";
|
||||||
import { OpordLeaderboard } from "./OpordLeaderboard";
|
import { OpordLeaderboard } from "./OpordLeaderboard";
|
||||||
|
|
||||||
const TIER_OPTIONS = [
|
const TIER_OPTIONS = [
|
||||||
{ value: "recruit", label: "Recruit", detail: "4 questions · 45s recall · core order" },
|
{ value: "recruit", label: "Recruit", detail: "10 questions · 110s recall · core order" },
|
||||||
{ value: "veteran", label: "Veteran", detail: "6 questions · 60s recall · +coordination annex" },
|
{ value: "veteran", label: "Veteran", detail: "15 questions · 165s recall · +coordination annex" },
|
||||||
{ value: "elite", label: "Elite", detail: "8 questions · 85s recall · full order" },
|
{ value: "elite", label: "Elite", detail: "20 questions · 225s recall · full order" },
|
||||||
] as const satisfies ReadonlyArray<{
|
] as const satisfies ReadonlyArray<{
|
||||||
readonly value: OpordDifficulty;
|
readonly value: OpordDifficulty;
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,14 @@ function buildBriefingRows(
|
||||||
push(rows, "subtitle", `OPERATIONS ORDER NO. ${String(scenario.seed).slice(-4).padStart(4, "0")}`);
|
push(rows, "subtitle", `OPERATIONS ORDER NO. ${String(scenario.seed).slice(-4).padStart(4, "0")}`);
|
||||||
rows.push({ kind: "rule" });
|
rows.push({ kind: "rule" });
|
||||||
push(rows, "header", "1. SITUATION");
|
push(rows, "header", "1. SITUATION");
|
||||||
|
const enemyStrength =
|
||||||
|
difficulty === "recruit"
|
||||||
|
? `${scenario.enemyVehicles} vehicle(s) and ${scenario.enemySquads} infantry squad(s)`
|
||||||
|
: `${scenario.enemyVehicles} vehicle(s), ${scenario.enemySquads} infantry squad(s), and ${scenario.enemyMortars} mortar team(s)`;
|
||||||
push(
|
push(
|
||||||
rows,
|
rows,
|
||||||
"text",
|
"text",
|
||||||
`Enemy: ${scenario.enemyVehicles} vehicle(s) and ${scenario.enemySquads} infantry squad(s) dug in on objective ${scenario.objective} at grid ${formatOpordGrid(scenario.grid)}.`,
|
`Enemy: ${enemyStrength} dug in on objective ${scenario.objective} at grid ${formatOpordGrid(scenario.grid)}.`,
|
||||||
);
|
);
|
||||||
push(rows, "text", `Friendly: ${scenario.friendlyUnit} in support aboard ${scenario.transport}.`);
|
push(rows, "text", `Friendly: ${scenario.friendlyUnit} in support aboard ${scenario.transport}.`);
|
||||||
push(rows, "header", "2. MISSION");
|
push(rows, "header", "2. MISSION");
|
||||||
|
|
@ -132,10 +136,25 @@ function buildBriefingRows(
|
||||||
`Movement from the line of departure to the objective takes ${scenario.enRouteMin} minutes.`,
|
`Movement from the line of departure to the objective takes ${scenario.enRouteMin} minutes.`,
|
||||||
);
|
);
|
||||||
push(rows, "text", `On abort, return to ${scenario.rallyPoint} and await further orders.`);
|
push(rows, "text", `On abort, return to ${scenario.rallyPoint} and await further orders.`);
|
||||||
|
push(rows, "text", `Resupply is scheduled at H+${scenario.resupplyOffsetMin} minutes.`);
|
||||||
|
push(rows, "text", `Exfiltration no later than H+${scenario.exfilOffsetMin} minutes.`);
|
||||||
|
push(
|
||||||
|
rows,
|
||||||
|
"text",
|
||||||
|
`Withdrawal route runs on azimuth ${String(scenario.withdrawalAzimuth).padStart(3, "0")}.`,
|
||||||
|
);
|
||||||
|
push(rows, "text", `Casualty evacuation lifts from ${scenario.casevacLz}.`);
|
||||||
}
|
}
|
||||||
push(rows, "header", difficulty === "elite" ? "6. COMMAND & SIGNAL" : "5. COMMAND & SIGNAL");
|
push(rows, "header", difficulty === "elite" ? "6. COMMAND & SIGNAL" : "5. COMMAND & SIGNAL");
|
||||||
rows.push({ kind: "kv", label: "AUTHENTICATION CODEWORD:", value: scenario.codeword });
|
rows.push({ kind: "kv", label: "AUTHENTICATION CODEWORD:", value: scenario.codeword });
|
||||||
rows.push({ kind: "kv", label: "ABORT CODEWORD:", value: scenario.abortCodeword });
|
rows.push({ kind: "kv", label: "ABORT CODEWORD:", value: scenario.abortCodeword });
|
||||||
|
if (difficulty === "elite") {
|
||||||
|
rows.push({
|
||||||
|
kind: "kv",
|
||||||
|
label: "STATUS CHECK-INS:",
|
||||||
|
value: `every ${scenario.checkInMinutes} minutes`,
|
||||||
|
});
|
||||||
|
}
|
||||||
rows.push({ kind: "gap" });
|
rows.push({ kind: "gap" });
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,30 +31,32 @@ export interface OpordTierConfig {
|
||||||
/** Grace added to the quiz limit for network/rounding before the server rejects. */
|
/** Grace added to the quiz limit for network/rounding before the server rejects. */
|
||||||
export const QUIZ_TIME_GRACE_MS = 2_500;
|
export const QUIZ_TIME_GRACE_MS = 2_500;
|
||||||
|
|
||||||
|
// Tiers keep the 4:6:8 ratio scaled to 10:15:20 questions; the recall clock
|
||||||
|
// (~11s/question) and par time (~8s/question) scale with the question count.
|
||||||
export const OPORD_DIFFICULTIES: Record<OpordDifficulty, OpordTierConfig> = {
|
export const OPORD_DIFFICULTIES: Record<OpordDifficulty, OpordTierConfig> = {
|
||||||
recruit: {
|
recruit: {
|
||||||
studyHintSeconds: 25,
|
studyHintSeconds: 30,
|
||||||
questionCount: 4,
|
questionCount: 10,
|
||||||
quizSeconds: 45,
|
quizSeconds: 110,
|
||||||
correctPoints: 100,
|
correctPoints: 100,
|
||||||
parTimeMs: 32_000,
|
parTimeMs: 80_000,
|
||||||
perfectBonus: 150,
|
perfectBonus: 150,
|
||||||
},
|
},
|
||||||
veteran: {
|
veteran: {
|
||||||
studyHintSeconds: 20,
|
studyHintSeconds: 25,
|
||||||
questionCount: 6,
|
questionCount: 15,
|
||||||
quizSeconds: 60,
|
quizSeconds: 165,
|
||||||
correctPoints: 150,
|
correctPoints: 150,
|
||||||
parTimeMs: 45_000,
|
parTimeMs: 120_000,
|
||||||
perfectBonus: 300,
|
perfectBonus: 300,
|
||||||
},
|
},
|
||||||
elite: {
|
elite: {
|
||||||
// Extra clock headroom: elite relational questions include time arithmetic.
|
// Extra clock headroom: elite relational questions include time arithmetic.
|
||||||
studyHintSeconds: 15,
|
studyHintSeconds: 20,
|
||||||
questionCount: 8,
|
questionCount: 20,
|
||||||
quizSeconds: 85,
|
quizSeconds: 225,
|
||||||
correctPoints: 200,
|
correctPoints: 200,
|
||||||
parTimeMs: 68_000,
|
parTimeMs: 160_000,
|
||||||
perfectBonus: 500,
|
perfectBonus: 500,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -178,6 +180,8 @@ const RALLY_POINT_NAMES = ["EAGLE", "WOLF", "TITAN", "SABER", "IRONHIDE", "BASTI
|
||||||
|
|
||||||
const AIRCRAFT_CALLSIGNS = ["RAVEN", "THUNDER", "HARRIER", "OSPREY", "COMET", "GHOST"] as const;
|
const AIRCRAFT_CALLSIGNS = ["RAVEN", "THUNDER", "HARRIER", "OSPREY", "COMET", "GHOST"] as const;
|
||||||
|
|
||||||
|
const LZ_NAMES = ["BISON", "TARPAN", "KESTREL", "MAMMOTH", "OBELISK", "SANDPIPER"] as const;
|
||||||
|
|
||||||
const CARDINAL_DIRECTIONS = [
|
const CARDINAL_DIRECTIONS = [
|
||||||
"NORTH", "NORTHEAST", "EAST", "SOUTHEAST", "SOUTH", "SOUTHWEST", "WEST", "NORTHWEST",
|
"NORTH", "NORTHEAST", "EAST", "SOUTHEAST", "SOUTH", "SOUTHWEST", "WEST", "NORTHWEST",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
@ -215,6 +219,18 @@ export interface OpordScenario {
|
||||||
readonly airSupportOffsetMin: number;
|
readonly airSupportOffsetMin: number;
|
||||||
/** Minutes of movement from the line of departure to the objective. */
|
/** Minutes of movement from the line of departure to the objective. */
|
||||||
readonly enRouteMin: number;
|
readonly enRouteMin: number;
|
||||||
|
/** Enemy mortar teams supporting the objective (1-3), rendered at veteran+ tiers. */
|
||||||
|
readonly enemyMortars: number;
|
||||||
|
/** Minutes between mandatory status check-ins with the TOC. */
|
||||||
|
readonly checkInMinutes: number;
|
||||||
|
/** e.g. "LZ KESTREL"; designated casualty evacuation landing zone. */
|
||||||
|
readonly casevacLz: string;
|
||||||
|
/** Minutes after H-HOUR when resupply reaches the objective (90-180). */
|
||||||
|
readonly resupplyOffsetMin: number;
|
||||||
|
/** Minutes after H-HOUR when the element must be off the objective; always later than resupply. */
|
||||||
|
readonly exfilOffsetMin: number;
|
||||||
|
/** Egress azimuth in degrees (1-359); never equals the main axis azimuth. */
|
||||||
|
readonly withdrawalAzimuth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Formats a 6-digit grid Arma map style: three digits, hyphen, three digits, e.g. "034-672". */
|
/** Formats a 6-digit grid Arma map style: three digits, hyphen, three digits, e.g. "034-672". */
|
||||||
|
|
@ -241,6 +257,13 @@ export function generateOpordScenario(seed: number): OpordScenario {
|
||||||
const abortCodeword = pick(rng, CODEWORDS.filter((word) => word !== codeword));
|
const abortCodeword = pick(rng, CODEWORDS.filter((word) => word !== codeword));
|
||||||
const callsign = `${pick(rng, CALLSIGNS)} ${pick(rng, [1, 2, 3, 5, 6, 7])}`;
|
const callsign = `${pick(rng, CALLSIGNS)} ${pick(rng, [1, 2, 3, 5, 6, 7])}`;
|
||||||
const frequency = `${pickInt(rng, 28, 45)}.${String(pickInt(rng, 0, 999)).padStart(3, "0")}`;
|
const frequency = `${pickInt(rng, 28, 45)}.${String(pickInt(rng, 0, 999)).padStart(3, "0")}`;
|
||||||
|
const resupplyOffsetMin = pick(rng, [90, 120, 150, 180]);
|
||||||
|
// Exfil always follows resupply, so the two timing questions never share an answer.
|
||||||
|
const exfilOffsetMin = resupplyOffsetMin + pick(rng, [30, 60, 90]);
|
||||||
|
// Offsets of 120-210 degrees keep the withdrawal clear of the main axis
|
||||||
|
// azimuth and its cardinal direction, so the two questions never collide.
|
||||||
|
const wrappedWithdrawal = (azimuth + pick(rng, [120, 150, 180, 210])) % 360;
|
||||||
|
const withdrawalAzimuth = wrappedWithdrawal === 0 ? 359 : wrappedWithdrawal;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
seed,
|
seed,
|
||||||
|
|
@ -258,12 +281,18 @@ export function generateOpordScenario(seed: number): OpordScenario {
|
||||||
frequency,
|
frequency,
|
||||||
enemyVehicles: pickInt(rng, 1, 4),
|
enemyVehicles: pickInt(rng, 1, 4),
|
||||||
enemySquads: pickInt(rng, 1, 3),
|
enemySquads: pickInt(rng, 1, 3),
|
||||||
|
enemyMortars: pickInt(rng, 1, 3),
|
||||||
friendlyUnit: pick(rng, FRIENDLY_UNITS),
|
friendlyUnit: pick(rng, FRIENDLY_UNITS),
|
||||||
transport: pick(rng, TRANSPORT_OPTIONS),
|
transport: pick(rng, TRANSPORT_OPTIONS),
|
||||||
rallyPoint: `RALLY POINT ${pick(rng, RALLY_POINT_NAMES)}`,
|
rallyPoint: `RALLY POINT ${pick(rng, RALLY_POINT_NAMES)}`,
|
||||||
aircraftCallsign: `${pick(rng, AIRCRAFT_CALLSIGNS)} ${pick(rng, [1, 2, 3, 4])}`,
|
aircraftCallsign: `${pick(rng, AIRCRAFT_CALLSIGNS)} ${pick(rng, [1, 2, 3, 4])}`,
|
||||||
airSupportOffsetMin: pick(rng, [15, 20, 30, 45]),
|
airSupportOffsetMin: pick(rng, [15, 20, 30, 45]),
|
||||||
enRouteMin: pickInt(rng, 18, 42),
|
enRouteMin: pickInt(rng, 18, 42),
|
||||||
|
checkInMinutes: pick(rng, [10, 15, 20, 30]),
|
||||||
|
casevacLz: `LZ ${pick(rng, LZ_NAMES)}`,
|
||||||
|
resupplyOffsetMin,
|
||||||
|
exfilOffsetMin,
|
||||||
|
withdrawalAzimuth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,6 +461,57 @@ export function buildOpordQuestions(
|
||||||
shuffle(rng, FRIENDLY_UNITS.filter((unit) => unit !== scenario.friendlyUnit)).slice(0, 3),
|
shuffle(rng, FRIENDLY_UNITS.filter((unit) => unit !== scenario.friendlyUnit)).slice(0, 3),
|
||||||
rng,
|
rng,
|
||||||
),
|
),
|
||||||
|
distance: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"distance",
|
||||||
|
"How far must the element advance to reach the objective?",
|
||||||
|
`${scenario.distanceKm} km`,
|
||||||
|
[scenario.distanceKm - 3, scenario.distanceKm + 2, scenario.distanceKm + 7]
|
||||||
|
.map((value) => Math.max(1, value))
|
||||||
|
.map((value) => `${value} km`),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
squads: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"squads",
|
||||||
|
"How many enemy infantry squads are on the objective?",
|
||||||
|
`${scenario.enemySquads}`,
|
||||||
|
["1", "2", "3", "4", "5"]
|
||||||
|
.filter((count) => count !== String(scenario.enemySquads))
|
||||||
|
.slice(0, 3),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
mortars: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"mortars",
|
||||||
|
"How many enemy mortar teams support the objective?",
|
||||||
|
`${scenario.enemyMortars}`,
|
||||||
|
["1", "2", "3", "4"]
|
||||||
|
.filter((count) => count !== String(scenario.enemyMortars))
|
||||||
|
.slice(0, 3),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
checkIn: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"check-in",
|
||||||
|
"How often must the element check in with the TOC?",
|
||||||
|
`Every ${scenario.checkInMinutes} minutes`,
|
||||||
|
[10, 15, 20, 30]
|
||||||
|
.filter((value) => value !== scenario.checkInMinutes)
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((value) => `Every ${value} minutes`),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
casevac: () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"casevac",
|
||||||
|
"Which landing zone is designated for casualty evacuation?",
|
||||||
|
scenario.casevacLz,
|
||||||
|
shuffle(rng, LZ_NAMES.filter((name) => scenario.casevacLz !== `LZ ${name}`))
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((name) => `LZ ${name}`),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Relational questions link two rendered briefing elements instead of
|
// Relational questions link two rendered briefing elements instead of
|
||||||
|
|
@ -517,6 +597,57 @@ export function buildOpordQuestions(
|
||||||
shuffle(rng, TRANSPORT_OPTIONS.filter((option) => option !== scenario.transport)).slice(0, 3),
|
shuffle(rng, TRANSPORT_OPTIONS.filter((option) => option !== scenario.transport)).slice(0, 3),
|
||||||
rng,
|
rng,
|
||||||
),
|
),
|
||||||
|
"pl-sequence": () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"pl-sequence",
|
||||||
|
"Which phase line does the element cross immediately after the line of departure?",
|
||||||
|
scenario.otherPhaseLines[0],
|
||||||
|
[
|
||||||
|
scenario.otherPhaseLines[1],
|
||||||
|
...shuffle(
|
||||||
|
rng,
|
||||||
|
PHASE_LINE_NAMES.filter(
|
||||||
|
(name) => name !== scenario.ldPhaseLine && !scenario.otherPhaseLines.includes(name),
|
||||||
|
),
|
||||||
|
).slice(0, 2),
|
||||||
|
],
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
"resupply-time": () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"resupply-time",
|
||||||
|
`Resupply is scheduled at H+${scenario.resupplyOffsetMin} minutes. What time is that (Zulu)?`,
|
||||||
|
zuluShift(scenario.hHour, scenario.resupplyOffsetMin),
|
||||||
|
[
|
||||||
|
zuluShift(scenario.hHour, scenario.resupplyOffsetMin - 15),
|
||||||
|
zuluShift(scenario.hHour, scenario.resupplyOffsetMin + 15),
|
||||||
|
zuluShift(scenario.hHour, scenario.resupplyOffsetMin + 30),
|
||||||
|
],
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
"exfil-time": () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"exfil-time",
|
||||||
|
`The element must be off the objective no later than H+${scenario.exfilOffsetMin} minutes. What is the latest (Zulu) exfil time?`,
|
||||||
|
zuluShift(scenario.hHour, scenario.exfilOffsetMin),
|
||||||
|
[
|
||||||
|
zuluShift(scenario.hHour, scenario.exfilOffsetMin - 15),
|
||||||
|
zuluShift(scenario.hHour, scenario.exfilOffsetMin + 15),
|
||||||
|
zuluShift(scenario.hHour, scenario.exfilOffsetMin + 30),
|
||||||
|
],
|
||||||
|
rng,
|
||||||
|
),
|
||||||
|
"withdrawal-cardinal": () =>
|
||||||
|
choiceQuestion(
|
||||||
|
"withdrawal-cardinal",
|
||||||
|
`The withdrawal route runs on azimuth ${pad3(scenario.withdrawalAzimuth)}. Which direction is that?`,
|
||||||
|
cardinalFromAzimuth(scenario.withdrawalAzimuth),
|
||||||
|
shuffle(
|
||||||
|
rng,
|
||||||
|
CARDINAL_DIRECTIONS.filter((dir) => dir !== cardinalFromAzimuth(scenario.withdrawalAzimuth)),
|
||||||
|
).slice(0, 3),
|
||||||
|
rng,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const tier = OPORD_DIFFICULTIES[difficulty];
|
const tier = OPORD_DIFFICULTIES[difficulty];
|
||||||
|
|
@ -531,6 +662,8 @@ export function buildOpordQuestions(
|
||||||
recallBuilders.enemy,
|
recallBuilders.enemy,
|
||||||
recallBuilders.callsign,
|
recallBuilders.callsign,
|
||||||
recallBuilders.friendly,
|
recallBuilders.friendly,
|
||||||
|
recallBuilders.distance,
|
||||||
|
recallBuilders.squads,
|
||||||
]
|
]
|
||||||
: difficulty === "veteran"
|
: difficulty === "veteran"
|
||||||
? [
|
? [
|
||||||
|
|
@ -541,6 +674,9 @@ export function buildOpordQuestions(
|
||||||
recallBuilders.enemy,
|
recallBuilders.enemy,
|
||||||
recallBuilders.objective,
|
recallBuilders.objective,
|
||||||
recallBuilders.friendly,
|
recallBuilders.friendly,
|
||||||
|
recallBuilders.distance,
|
||||||
|
recallBuilders.squads,
|
||||||
|
recallBuilders.mortars,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
recallBuilders.codeword,
|
recallBuilders.codeword,
|
||||||
|
|
@ -550,6 +686,12 @@ export function buildOpordQuestions(
|
||||||
recallBuilders.objective,
|
recallBuilders.objective,
|
||||||
recallBuilders.friendly,
|
recallBuilders.friendly,
|
||||||
recallBuilders["h-hour"],
|
recallBuilders["h-hour"],
|
||||||
|
recallBuilders.frequency,
|
||||||
|
recallBuilders.distance,
|
||||||
|
recallBuilders.squads,
|
||||||
|
recallBuilders.mortars,
|
||||||
|
recallBuilders.checkIn,
|
||||||
|
recallBuilders.casevac,
|
||||||
];
|
];
|
||||||
const relationalPool: readonly (() => OpordQuestion)[] =
|
const relationalPool: readonly (() => OpordQuestion)[] =
|
||||||
difficulty === "veteran"
|
difficulty === "veteran"
|
||||||
|
|
@ -557,17 +699,24 @@ export function buildOpordQuestions(
|
||||||
relationalBuilders["azimuth-cardinal"],
|
relationalBuilders["azimuth-cardinal"],
|
||||||
relationalBuilders["time-cas"],
|
relationalBuilders["time-cas"],
|
||||||
relationalBuilders["phase-first"],
|
relationalBuilders["phase-first"],
|
||||||
|
relationalBuilders["pl-sequence"],
|
||||||
|
relationalBuilders["transport-link"],
|
||||||
|
relationalBuilders["callsign-net"],
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
relationalBuilders["azimuth-cardinal"],
|
relationalBuilders["azimuth-cardinal"],
|
||||||
relationalBuilders["time-cas"],
|
relationalBuilders["time-cas"],
|
||||||
relationalBuilders["phase-first"],
|
relationalBuilders["phase-first"],
|
||||||
|
relationalBuilders["pl-sequence"],
|
||||||
relationalBuilders["movement-eta"],
|
relationalBuilders["movement-eta"],
|
||||||
relationalBuilders["abort-rally"],
|
relationalBuilders["abort-rally"],
|
||||||
relationalBuilders["callsign-net"],
|
relationalBuilders["callsign-net"],
|
||||||
relationalBuilders["transport-link"],
|
relationalBuilders["transport-link"],
|
||||||
|
relationalBuilders["resupply-time"],
|
||||||
|
relationalBuilders["exfil-time"],
|
||||||
|
relationalBuilders["withdrawal-cardinal"],
|
||||||
];
|
];
|
||||||
const relationalCount = difficulty === "recruit" ? 0 : difficulty === "veteran" ? 2 : 4;
|
const relationalCount = difficulty === "recruit" ? 0 : difficulty === "veteran" ? 5 : 8;
|
||||||
const relational = shuffle(rng, relationalPool).slice(0, relationalCount);
|
const relational = shuffle(rng, relationalPool).slice(0, relationalCount);
|
||||||
const recall = shuffle(rng, recallPool).slice(0, tier.questionCount - 1 - relationalCount);
|
const recall = shuffle(rng, recallPool).slice(0, tier.questionCount - 1 - relationalCount);
|
||||||
return [gridBuilder(), ...shuffle(rng, [...relational, ...recall]).map((build) => build())];
|
return [gridBuilder(), ...shuffle(rng, [...relational, ...recall]).map((build) => build())];
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,13 @@ async function playOpordRound(page: Page): Promise<void> {
|
||||||
|
|
||||||
const questionCounter = page.getByText(/Question \d+ of \d+/);
|
const questionCounter = page.getByText(/Question \d+ of \d+/);
|
||||||
await expect(questionCounter).toBeVisible();
|
await expect(questionCounter).toBeVisible();
|
||||||
await expect(questionCounter).toContainText("Question 1 of 4");
|
await expect(questionCounter).toContainText("Question 1 of 10");
|
||||||
|
|
||||||
// Question 1 is always the objective grid: answer it correctly.
|
// Question 1 is always the objective grid: answer it correctly.
|
||||||
await page.locator(QUIZ_OPTION_BUTTON).filter({ hasText: grid }).first().click();
|
await page.locator(QUIZ_OPTION_BUTTON).filter({ hasText: grid }).first().click();
|
||||||
|
|
||||||
for (let index = 1; index < 4; index += 1) {
|
for (let index = 1; index < 10; index += 1) {
|
||||||
await expect(questionCounter).toContainText(`Question ${index + 1} of 4`, {
|
await expect(questionCounter).toContainText(`Question ${index + 1} of 10`, {
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
});
|
});
|
||||||
await page.locator(QUIZ_OPTION_BUTTON).first().click();
|
await page.locator(QUIZ_OPTION_BUTTON).first().click();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue