@playcademy/sandbox 0.10.1-beta.3 → 0.10.1-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -285,7 +285,7 @@ var init_platform = __esm(() => {
285
285
  var PLATFORM_TIMEZONE = "America/New_York";
286
286
 
287
287
  // ../constants/src/timeback.ts
288
- var TIMEBACK_GRADES, TIMEBACK_SUBJECTS, ASSESSMENT_PURPOSES, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION = 1, PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION = 1, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY = "playcademyAssessment", PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY = "playcademyAssessmentItem", TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH = 2048, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
288
+ var TIMEBACK_GRADES, TIMEBACK_SUBJECTS, ASSESSMENT_PURPOSES, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION = 1, PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION = 2, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY = "playcademyAssessment", PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY = "playcademyAssessmentItem", TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH = 2048, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
289
289
  var init_timeback2 = __esm(() => {
290
290
  TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
291
291
  TIMEBACK_SUBJECTS = [
@@ -1128,7 +1128,7 @@ var package_default;
1128
1128
  var init_package = __esm(() => {
1129
1129
  package_default = {
1130
1130
  name: "@playcademy/sandbox",
1131
- version: "0.10.1-beta.3",
1131
+ version: "0.10.1-beta.4",
1132
1132
  description: "Local development server for Playcademy game development",
1133
1133
  type: "module",
1134
1134
  exports: {
@@ -10834,30 +10834,60 @@ function comparePresentationText(left, right) {
10834
10834
  }
10835
10835
  return 0;
10836
10836
  }
10837
+ function presentationRandomIndex(seed) {
10838
+ let state = presentationHash(seed);
10839
+ const range = 2 ** 32;
10840
+ return (bound) => {
10841
+ const limit = range - range % bound;
10842
+ let value;
10843
+ do {
10844
+ state = state + 1831565813 >>> 0;
10845
+ let mixed = Math.imul(state ^ state >>> 15, state | 1);
10846
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
10847
+ value = (mixed ^ mixed >>> 14) >>> 0;
10848
+ } while (value >= limit);
10849
+ return value % bound;
10850
+ };
10851
+ }
10837
10852
  function presentationChoices(choices, attemptId, itemIdentifier, domain) {
10838
- const seed = `${attemptId}\x00${itemIdentifier}\x00${domain}\x00`;
10839
- return [...choices].toSorted((left, right) => {
10840
- const hashDifference = presentationHash(`${seed}${left.identifier}`) - presentationHash(`${seed}${right.identifier}`);
10841
- return hashDifference || comparePresentationText(left.identifier, right.identifier);
10842
- });
10853
+ const randomIndex = presentationRandomIndex(JSON.stringify([attemptId, itemIdentifier, domain]));
10854
+ const movable = choices.filter((choice) => !choice.fixed).toSorted((left, right) => comparePresentationText(left.identifier, right.identifier));
10855
+ for (let index = movable.length - 1;index > 0; index--) {
10856
+ const other = randomIndex(index + 1);
10857
+ [movable[index], movable[other]] = [movable[other], movable[index]];
10858
+ }
10859
+ let next = 0;
10860
+ return choices.map((choice) => choice.fixed ? choice : movable[next++]);
10843
10861
  }
10844
10862
  function assessmentPresentationForAttempt(assessment, attemptId) {
10845
10863
  const items = assessment.items.map((item) => {
10846
10864
  const interactions = item.interactions.map((interaction) => {
10847
- if (interaction.type === "order" && interaction.shuffle) {
10848
- return {
10849
- ...interaction,
10850
- choices: presentationChoices(interaction.choices, attemptId, item.identifier, `order\x00${interaction.responseIdentifier}`)
10851
- };
10865
+ if (!("shuffle" in interaction) || !interaction.shuffle) {
10866
+ return interaction;
10852
10867
  }
10853
- if (interaction.type === "match" && interaction.shuffle) {
10854
- return {
10855
- ...interaction,
10856
- sourceChoices: presentationChoices(interaction.sourceChoices, attemptId, item.identifier, `match-source\x00${interaction.responseIdentifier}`),
10857
- targetChoices: presentationChoices(interaction.targetChoices, attemptId, item.identifier, `match-target\x00${interaction.responseIdentifier}`)
10858
- };
10868
+ function shuffled(choices, domain) {
10869
+ return presentationChoices(choices, attemptId, item.identifier, `${domain}\x00${interaction.responseIdentifier}`);
10870
+ }
10871
+ switch (interaction.type) {
10872
+ case "choice":
10873
+ case "inline-choice":
10874
+ case "order": {
10875
+ return {
10876
+ ...interaction,
10877
+ choices: shuffled(interaction.choices, interaction.type)
10878
+ };
10879
+ }
10880
+ case "gap-match": {
10881
+ return { ...interaction, gapTexts: shuffled(interaction.gapTexts, "gap-match") };
10882
+ }
10883
+ case "match": {
10884
+ return {
10885
+ ...interaction,
10886
+ sourceChoices: shuffled(interaction.sourceChoices, "match-source"),
10887
+ targetChoices: shuffled(interaction.targetChoices, "match-target")
10888
+ };
10889
+ }
10859
10890
  }
10860
- return interaction;
10861
10891
  });
10862
10892
  return { ...item, interactions };
10863
10893
  });
@@ -11812,6 +11842,7 @@ var init_assessment_runtime2 = __esm(() => {
11812
11842
  init_src();
11813
11843
  init_timeback3();
11814
11844
  init_src();
11845
+ init_src();
11815
11846
  init_uuid();
11816
11847
  init_esm();
11817
11848
  init_src();
@@ -39209,7 +39240,7 @@ function assertMatchingAssessmentArtifactPair(playable, scoring, expectedIdentit
39209
39240
  throw new ValidationError("Assessment artifact pair does not describe one revision");
39210
39241
  }
39211
39242
  }
39212
- var ASSESSMENT_ARTIFACT_SCHEMA_VERSION = 1, ASSESSMENT_GRADER_PROTOCOL_VERSION = 1, ARTIFACT_KEY_PREFIX = "assessment-artifacts", SHA256_HEX, textEncoder;
39243
+ var ASSESSMENT_ARTIFACT_SCHEMA_VERSION = 2, ASSESSMENT_GRADER_PROTOCOL_VERSION = 1, ARTIFACT_KEY_PREFIX = "assessment-artifacts", SHA256_HEX, textEncoder;
39213
39244
  var init_assessment_artifact_envelope_util = __esm(() => {
39214
39245
  init_errors2();
39215
39246
  SHA256_HEX = /^[a-f0-9]{64}$/;
@@ -94303,6 +94334,30 @@ var init_timeback_game_metrics_comparison_util = __esm(() => {
94303
94334
  });
94304
94335
 
94305
94336
  // ../timeback/dist/engines/qti/index.js
94337
+ function qtiAttributeNumber(attributes, name3, fallback) {
94338
+ const raw = attributes[name3]?.trim();
94339
+ if (!raw) {
94340
+ return fallback;
94341
+ }
94342
+ const value = Number(raw);
94343
+ return Number.isFinite(value) ? value : fallback;
94344
+ }
94345
+ function qtiAttributeCapacity(attributes, name3, fallback) {
94346
+ const raw = attributes[name3]?.trim();
94347
+ if (!raw) {
94348
+ return fallback;
94349
+ }
94350
+ const value = Number(raw);
94351
+ return Number.isInteger(value) && value >= 0 ? value : null;
94352
+ }
94353
+ function qtiAttributeMaximum(attributes, name3, fallback, unlimited) {
94354
+ const maximum = qtiAttributeNumber(attributes, name3, fallback);
94355
+ return maximum === 0 ? unlimited : maximum;
94356
+ }
94357
+ function qtiAttributeBoolean(attributes, name3) {
94358
+ const value = attributes[name3];
94359
+ return value === "true" || value === "1";
94360
+ }
94306
94361
  function decodeCssEscapes(value) {
94307
94362
  return value.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_, hex3) => {
94308
94363
  const code = Number.parseInt(hex3, 16);
@@ -94890,7 +94945,8 @@ function parseGapTexts(interaction) {
94890
94945
  return {
94891
94946
  identifier: attributes2.identifier || "",
94892
94947
  content: plainTextFromNodes(gapText.children),
94893
- matchMax: finiteNumber(attributes2["match-max"]) ?? 1
94948
+ matchMax: finiteNumber(attributes2["match-max"]) ?? 1,
94949
+ ...qtiAttributeBoolean(attributes2, "fixed") ? { fixed: true } : {}
94894
94950
  };
94895
94951
  }).filter((gapText) => gapText.identifier);
94896
94952
  }
@@ -95990,30 +96046,6 @@ ${body2}
95990
96046
  ${processing}` : ""}
95991
96047
  </qti-assessment-item>`;
95992
96048
  }
95993
- function qtiAttributeNumber(attributes2, name3, fallback) {
95994
- const raw = attributes2[name3]?.trim();
95995
- if (!raw) {
95996
- return fallback;
95997
- }
95998
- const value = Number(raw);
95999
- return Number.isFinite(value) ? value : fallback;
96000
- }
96001
- function qtiAttributeCapacity(attributes2, name3, fallback) {
96002
- const raw = attributes2[name3]?.trim();
96003
- if (!raw) {
96004
- return fallback;
96005
- }
96006
- const value = Number(raw);
96007
- return Number.isInteger(value) && value >= 0 ? value : null;
96008
- }
96009
- function qtiAttributeMaximum(attributes2, name3, fallback, unlimited) {
96010
- const maximum = qtiAttributeNumber(attributes2, name3, fallback);
96011
- return maximum === 0 ? unlimited : maximum;
96012
- }
96013
- function qtiAttributeBoolean(attributes2, name3) {
96014
- const value = attributes2[name3];
96015
- return value === "true" || value === "1";
96016
- }
96017
96049
  function gapContentIdentifiers(nodes) {
96018
96050
  return nodes.flatMap((node) => {
96019
96051
  if (node.kind === "gap") {
@@ -96094,6 +96126,7 @@ function authorableQtiInteractionProjection(interaction) {
96094
96126
  type: interaction.type,
96095
96127
  choices: interaction.choices,
96096
96128
  minChoices: qtiAttributeNumber(attributes2, "min-choices", 1),
96129
+ shuffle: qtiAttributeBoolean(attributes2, "shuffle"),
96097
96130
  maxChoices: qtiAttributeMaximum(attributes2, "max-choices", 1, interaction.choices.length)
96098
96131
  };
96099
96132
  }
@@ -96102,6 +96135,7 @@ function authorableQtiInteractionProjection(interaction) {
96102
96135
  ...base,
96103
96136
  type: interaction.type,
96104
96137
  choices: interaction.choices,
96138
+ shuffle: qtiAttributeBoolean(attributes2, "shuffle"),
96105
96139
  blankIndex: interaction.blankIndex ?? 0
96106
96140
  } : null;
96107
96141
  }
@@ -99933,11 +99967,13 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99933
99967
  playableContentNodesSchema = exports_external.array(exports_external.unknown());
99934
99968
  playableChoiceSchema = exports_external.object({
99935
99969
  identifier: trimmedNonEmptyString3,
99970
+ fixed: exports_external.boolean().optional(),
99936
99971
  content: exports_external.string(),
99937
99972
  contentNodes: playableContentNodesSchema.optional()
99938
99973
  }).strict();
99939
99974
  playableMatchChoiceSchema = exports_external.object({
99940
99975
  identifier: trimmedNonEmptyString3,
99976
+ fixed: exports_external.boolean().optional(),
99941
99977
  content: exports_external.string(),
99942
99978
  contentNodes: playableContentNodesSchema.optional(),
99943
99979
  matchMax: nonnegativeInteger
@@ -99957,6 +99993,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99957
99993
  }).strict();
99958
99994
  playableGapTextSchema = exports_external.object({
99959
99995
  identifier: trimmedNonEmptyString3,
99996
+ fixed: exports_external.boolean().optional(),
99960
99997
  content: exports_external.string(),
99961
99998
  matchMax: nonnegativeInteger
99962
99999
  }).strict();
@@ -99974,6 +100011,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99974
100011
  exports_external.object({
99975
100012
  ...playableInteractionBase,
99976
100013
  type: exports_external.literal("choice"),
100014
+ shuffle: exports_external.boolean(),
99977
100015
  choices: exports_external.array(playableChoiceSchema),
99978
100016
  minChoices: nonnegativeInteger,
99979
100017
  maxChoices: nonnegativeInteger
@@ -99981,6 +100019,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99981
100019
  exports_external.object({
99982
100020
  ...playableInteractionBase,
99983
100021
  type: exports_external.literal("inline-choice"),
100022
+ shuffle: exports_external.boolean(),
99984
100023
  choices: exports_external.array(playableChoiceSchema)
99985
100024
  }).strict(),
99986
100025
  exports_external.object({
@@ -100024,6 +100063,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
100024
100063
  exports_external.object({
100025
100064
  ...playableInteractionBase,
100026
100065
  type: exports_external.literal("gap-match"),
100066
+ shuffle: exports_external.boolean(),
100027
100067
  content: exports_external.string(),
100028
100068
  contentNodes: playableContentNodesSchema,
100029
100069
  gapTexts: exports_external.array(playableGapTextSchema),
@@ -100536,6 +100576,7 @@ function playableChoice(choice) {
100536
100576
  return {
100537
100577
  identifier: choice.identifier,
100538
100578
  content: choice.content,
100579
+ ...qtiAttributeBoolean(choice.attributes, "fixed") ? { fixed: true } : {},
100539
100580
  ...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
100540
100581
  };
100541
100582
  }
@@ -100598,6 +100639,7 @@ function parseInteraction2(questionIdentifier, interaction) {
100598
100639
  responseIdentifier,
100599
100640
  ...prompt,
100600
100641
  choices,
100642
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle"),
100601
100643
  minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
100602
100644
  maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, choices.length)
100603
100645
  };
@@ -100607,7 +100649,8 @@ function parseInteraction2(questionIdentifier, interaction) {
100607
100649
  type,
100608
100650
  responseIdentifier,
100609
100651
  ...prompt,
100610
- choices: requireChoices2(interaction.choices.map(playableChoice))
100652
+ choices: requireChoices2(interaction.choices.map(playableChoice)),
100653
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
100611
100654
  };
100612
100655
  }
100613
100656
  if (type === "text-entry") {
@@ -100668,11 +100711,13 @@ function parseInteraction2(questionIdentifier, interaction) {
100668
100711
  responseIdentifier,
100669
100712
  ...prompt,
100670
100713
  content: interaction.textWithGaps,
100714
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle"),
100671
100715
  contentNodes: interaction.gapContent,
100672
100716
  gapTexts: interaction.gapTexts.map((gapText) => ({
100673
100717
  identifier: gapText.identifier,
100674
100718
  content: gapText.content,
100675
- matchMax: gapText.matchMax
100719
+ matchMax: gapText.matchMax,
100720
+ ...gapText.fixed ? { fixed: true } : {}
100676
100721
  })),
100677
100722
  gaps: [...interaction.gaps],
100678
100723
  maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
@@ -101491,7 +101536,7 @@ function buildInlineChoiceQuestionXml(input, identifier, title) {
101491
101536
  if (!envelope) {
101492
101537
  return null;
101493
101538
  }
101494
- const { structure, responseIdentifier, prompt } = envelope;
101539
+ const { interaction, structure, responseIdentifier, prompt } = envelope;
101495
101540
  const promptParts = prompt.split(INLINE_CHOICE_BLANK);
101496
101541
  if (promptParts.length !== 2) {
101497
101542
  throw new ValidationError("Inline-choice questions require exactly one blank");
@@ -101505,7 +101550,7 @@ function buildInlineChoiceQuestionXml(input, identifier, title) {
101505
101550
  const choicesXml = choices.map((choice) => ` <qti-inline-choice identifier="${escapeXml(choice.identifier)}">${escapeXml(choice.content)}</qti-inline-choice>`).join(`
101506
101551
  `);
101507
101552
  const [before = "", after = ""] = promptParts;
101508
- const itemBody = ` <p>${escapeXml(before)}<qti-inline-choice-interaction response-identifier="${escapeXml(responseIdentifier)}">
101553
+ const itemBody = ` <p>${escapeXml(before)}<qti-inline-choice-interaction response-identifier="${escapeXml(responseIdentifier)}" shuffle="${interaction.shuffle === true}">
101509
101554
  ${choicesXml}
101510
101555
  </qti-inline-choice-interaction>${escapeXml(after)}</p>`;
101511
101556
  return buildExactMatchQuestionXml({
@@ -102120,7 +102165,7 @@ async function prepareReviewMappingUpdate(test, questions) {
102120
102165
  const contentRevision = await assessmentContentRevision(test, questions);
102121
102166
  const assessment = {
102122
102167
  identifier: test.identifier,
102123
- contractVersion: 1,
102168
+ contractVersion: PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION,
102124
102169
  contentRevision,
102125
102170
  title: test.title,
102126
102171
  items: questions.questions.map(({ question }) => ({
@@ -102159,6 +102204,7 @@ async function prepareReviewMappingUpdate(test, questions) {
102159
102204
  var PLAYCADEMY_REVIEW_BANK_TITLE = "Review Bank", PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink", PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1, StoredSinkWarningSchema, ReviewBankSinkMarkerSchema;
102160
102205
  var init_timeback_review_mapping_util = __esm(() => {
102161
102206
  init_esm();
102207
+ init_src();
102162
102208
  init_assessment_runtime2();
102163
102209
  init_qti();
102164
102210
  init_timeback3();
package/dist/constants.js CHANGED
@@ -100,7 +100,7 @@ var init_platform = __esm(() => {
100
100
  var PLATFORM_TIMEZONE = "America/New_York";
101
101
 
102
102
  // ../constants/src/timeback.ts
103
- var TIMEBACK_GRADES, TIMEBACK_SUBJECTS, ASSESSMENT_PURPOSES, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION = 1, PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION = 1, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY = "playcademyAssessment", PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY = "playcademyAssessmentItem", TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH = 2048, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
103
+ var TIMEBACK_GRADES, TIMEBACK_SUBJECTS, ASSESSMENT_PURPOSES, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION = 1, PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION = 2, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY = "playcademyAssessment", PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY = "playcademyAssessmentItem", TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH = 2048, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
104
104
  var init_timeback = __esm(() => {
105
105
  TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
106
106
  TIMEBACK_SUBJECTS = [
package/dist/server.js CHANGED
@@ -284,7 +284,7 @@ var init_platform = __esm(() => {
284
284
  var PLATFORM_TIMEZONE = "America/New_York";
285
285
 
286
286
  // ../constants/src/timeback.ts
287
- var TIMEBACK_GRADES, TIMEBACK_SUBJECTS, ASSESSMENT_PURPOSES, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION = 1, PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION = 1, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY = "playcademyAssessment", PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY = "playcademyAssessmentItem", TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH = 2048, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
287
+ var TIMEBACK_GRADES, TIMEBACK_SUBJECTS, ASSESSMENT_PURPOSES, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION = 1, PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION = 2, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY = "playcademyAssessment", PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY = "playcademyAssessmentItem", TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH = 2048, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
288
288
  var init_timeback2 = __esm(() => {
289
289
  TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
290
290
  TIMEBACK_SUBJECTS = [
@@ -1127,7 +1127,7 @@ var package_default;
1127
1127
  var init_package = __esm(() => {
1128
1128
  package_default = {
1129
1129
  name: "@playcademy/sandbox",
1130
- version: "0.10.1-beta.3",
1130
+ version: "0.10.1-beta.4",
1131
1131
  description: "Local development server for Playcademy game development",
1132
1132
  type: "module",
1133
1133
  exports: {
@@ -10833,30 +10833,60 @@ function comparePresentationText(left, right) {
10833
10833
  }
10834
10834
  return 0;
10835
10835
  }
10836
+ function presentationRandomIndex(seed) {
10837
+ let state = presentationHash(seed);
10838
+ const range = 2 ** 32;
10839
+ return (bound) => {
10840
+ const limit = range - range % bound;
10841
+ let value;
10842
+ do {
10843
+ state = state + 1831565813 >>> 0;
10844
+ let mixed = Math.imul(state ^ state >>> 15, state | 1);
10845
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
10846
+ value = (mixed ^ mixed >>> 14) >>> 0;
10847
+ } while (value >= limit);
10848
+ return value % bound;
10849
+ };
10850
+ }
10836
10851
  function presentationChoices(choices, attemptId, itemIdentifier, domain) {
10837
- const seed = `${attemptId}\x00${itemIdentifier}\x00${domain}\x00`;
10838
- return [...choices].toSorted((left, right) => {
10839
- const hashDifference = presentationHash(`${seed}${left.identifier}`) - presentationHash(`${seed}${right.identifier}`);
10840
- return hashDifference || comparePresentationText(left.identifier, right.identifier);
10841
- });
10852
+ const randomIndex = presentationRandomIndex(JSON.stringify([attemptId, itemIdentifier, domain]));
10853
+ const movable = choices.filter((choice) => !choice.fixed).toSorted((left, right) => comparePresentationText(left.identifier, right.identifier));
10854
+ for (let index = movable.length - 1;index > 0; index--) {
10855
+ const other = randomIndex(index + 1);
10856
+ [movable[index], movable[other]] = [movable[other], movable[index]];
10857
+ }
10858
+ let next = 0;
10859
+ return choices.map((choice) => choice.fixed ? choice : movable[next++]);
10842
10860
  }
10843
10861
  function assessmentPresentationForAttempt(assessment, attemptId) {
10844
10862
  const items = assessment.items.map((item) => {
10845
10863
  const interactions = item.interactions.map((interaction) => {
10846
- if (interaction.type === "order" && interaction.shuffle) {
10847
- return {
10848
- ...interaction,
10849
- choices: presentationChoices(interaction.choices, attemptId, item.identifier, `order\x00${interaction.responseIdentifier}`)
10850
- };
10864
+ if (!("shuffle" in interaction) || !interaction.shuffle) {
10865
+ return interaction;
10851
10866
  }
10852
- if (interaction.type === "match" && interaction.shuffle) {
10853
- return {
10854
- ...interaction,
10855
- sourceChoices: presentationChoices(interaction.sourceChoices, attemptId, item.identifier, `match-source\x00${interaction.responseIdentifier}`),
10856
- targetChoices: presentationChoices(interaction.targetChoices, attemptId, item.identifier, `match-target\x00${interaction.responseIdentifier}`)
10857
- };
10867
+ function shuffled(choices, domain) {
10868
+ return presentationChoices(choices, attemptId, item.identifier, `${domain}\x00${interaction.responseIdentifier}`);
10869
+ }
10870
+ switch (interaction.type) {
10871
+ case "choice":
10872
+ case "inline-choice":
10873
+ case "order": {
10874
+ return {
10875
+ ...interaction,
10876
+ choices: shuffled(interaction.choices, interaction.type)
10877
+ };
10878
+ }
10879
+ case "gap-match": {
10880
+ return { ...interaction, gapTexts: shuffled(interaction.gapTexts, "gap-match") };
10881
+ }
10882
+ case "match": {
10883
+ return {
10884
+ ...interaction,
10885
+ sourceChoices: shuffled(interaction.sourceChoices, "match-source"),
10886
+ targetChoices: shuffled(interaction.targetChoices, "match-target")
10887
+ };
10888
+ }
10858
10889
  }
10859
- return interaction;
10860
10890
  });
10861
10891
  return { ...item, interactions };
10862
10892
  });
@@ -11811,6 +11841,7 @@ var init_assessment_runtime2 = __esm(() => {
11811
11841
  init_src();
11812
11842
  init_timeback3();
11813
11843
  init_src();
11844
+ init_src();
11814
11845
  init_uuid();
11815
11846
  init_esm();
11816
11847
  init_src();
@@ -39208,7 +39239,7 @@ function assertMatchingAssessmentArtifactPair(playable, scoring, expectedIdentit
39208
39239
  throw new ValidationError("Assessment artifact pair does not describe one revision");
39209
39240
  }
39210
39241
  }
39211
- var ASSESSMENT_ARTIFACT_SCHEMA_VERSION = 1, ASSESSMENT_GRADER_PROTOCOL_VERSION = 1, ARTIFACT_KEY_PREFIX = "assessment-artifacts", SHA256_HEX, textEncoder;
39242
+ var ASSESSMENT_ARTIFACT_SCHEMA_VERSION = 2, ASSESSMENT_GRADER_PROTOCOL_VERSION = 1, ARTIFACT_KEY_PREFIX = "assessment-artifacts", SHA256_HEX, textEncoder;
39212
39243
  var init_assessment_artifact_envelope_util = __esm(() => {
39213
39244
  init_errors2();
39214
39245
  SHA256_HEX = /^[a-f0-9]{64}$/;
@@ -94302,6 +94333,30 @@ var init_timeback_game_metrics_comparison_util = __esm(() => {
94302
94333
  });
94303
94334
 
94304
94335
  // ../timeback/dist/engines/qti/index.js
94336
+ function qtiAttributeNumber(attributes, name3, fallback) {
94337
+ const raw = attributes[name3]?.trim();
94338
+ if (!raw) {
94339
+ return fallback;
94340
+ }
94341
+ const value = Number(raw);
94342
+ return Number.isFinite(value) ? value : fallback;
94343
+ }
94344
+ function qtiAttributeCapacity(attributes, name3, fallback) {
94345
+ const raw = attributes[name3]?.trim();
94346
+ if (!raw) {
94347
+ return fallback;
94348
+ }
94349
+ const value = Number(raw);
94350
+ return Number.isInteger(value) && value >= 0 ? value : null;
94351
+ }
94352
+ function qtiAttributeMaximum(attributes, name3, fallback, unlimited) {
94353
+ const maximum = qtiAttributeNumber(attributes, name3, fallback);
94354
+ return maximum === 0 ? unlimited : maximum;
94355
+ }
94356
+ function qtiAttributeBoolean(attributes, name3) {
94357
+ const value = attributes[name3];
94358
+ return value === "true" || value === "1";
94359
+ }
94305
94360
  function decodeCssEscapes(value) {
94306
94361
  return value.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_, hex3) => {
94307
94362
  const code = Number.parseInt(hex3, 16);
@@ -94889,7 +94944,8 @@ function parseGapTexts(interaction) {
94889
94944
  return {
94890
94945
  identifier: attributes2.identifier || "",
94891
94946
  content: plainTextFromNodes(gapText.children),
94892
- matchMax: finiteNumber(attributes2["match-max"]) ?? 1
94947
+ matchMax: finiteNumber(attributes2["match-max"]) ?? 1,
94948
+ ...qtiAttributeBoolean(attributes2, "fixed") ? { fixed: true } : {}
94893
94949
  };
94894
94950
  }).filter((gapText) => gapText.identifier);
94895
94951
  }
@@ -95989,30 +96045,6 @@ ${body2}
95989
96045
  ${processing}` : ""}
95990
96046
  </qti-assessment-item>`;
95991
96047
  }
95992
- function qtiAttributeNumber(attributes2, name3, fallback) {
95993
- const raw = attributes2[name3]?.trim();
95994
- if (!raw) {
95995
- return fallback;
95996
- }
95997
- const value = Number(raw);
95998
- return Number.isFinite(value) ? value : fallback;
95999
- }
96000
- function qtiAttributeCapacity(attributes2, name3, fallback) {
96001
- const raw = attributes2[name3]?.trim();
96002
- if (!raw) {
96003
- return fallback;
96004
- }
96005
- const value = Number(raw);
96006
- return Number.isInteger(value) && value >= 0 ? value : null;
96007
- }
96008
- function qtiAttributeMaximum(attributes2, name3, fallback, unlimited) {
96009
- const maximum = qtiAttributeNumber(attributes2, name3, fallback);
96010
- return maximum === 0 ? unlimited : maximum;
96011
- }
96012
- function qtiAttributeBoolean(attributes2, name3) {
96013
- const value = attributes2[name3];
96014
- return value === "true" || value === "1";
96015
- }
96016
96048
  function gapContentIdentifiers(nodes) {
96017
96049
  return nodes.flatMap((node) => {
96018
96050
  if (node.kind === "gap") {
@@ -96093,6 +96125,7 @@ function authorableQtiInteractionProjection(interaction) {
96093
96125
  type: interaction.type,
96094
96126
  choices: interaction.choices,
96095
96127
  minChoices: qtiAttributeNumber(attributes2, "min-choices", 1),
96128
+ shuffle: qtiAttributeBoolean(attributes2, "shuffle"),
96096
96129
  maxChoices: qtiAttributeMaximum(attributes2, "max-choices", 1, interaction.choices.length)
96097
96130
  };
96098
96131
  }
@@ -96101,6 +96134,7 @@ function authorableQtiInteractionProjection(interaction) {
96101
96134
  ...base,
96102
96135
  type: interaction.type,
96103
96136
  choices: interaction.choices,
96137
+ shuffle: qtiAttributeBoolean(attributes2, "shuffle"),
96104
96138
  blankIndex: interaction.blankIndex ?? 0
96105
96139
  } : null;
96106
96140
  }
@@ -99932,11 +99966,13 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99932
99966
  playableContentNodesSchema = exports_external.array(exports_external.unknown());
99933
99967
  playableChoiceSchema = exports_external.object({
99934
99968
  identifier: trimmedNonEmptyString3,
99969
+ fixed: exports_external.boolean().optional(),
99935
99970
  content: exports_external.string(),
99936
99971
  contentNodes: playableContentNodesSchema.optional()
99937
99972
  }).strict();
99938
99973
  playableMatchChoiceSchema = exports_external.object({
99939
99974
  identifier: trimmedNonEmptyString3,
99975
+ fixed: exports_external.boolean().optional(),
99940
99976
  content: exports_external.string(),
99941
99977
  contentNodes: playableContentNodesSchema.optional(),
99942
99978
  matchMax: nonnegativeInteger
@@ -99956,6 +99992,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99956
99992
  }).strict();
99957
99993
  playableGapTextSchema = exports_external.object({
99958
99994
  identifier: trimmedNonEmptyString3,
99995
+ fixed: exports_external.boolean().optional(),
99959
99996
  content: exports_external.string(),
99960
99997
  matchMax: nonnegativeInteger
99961
99998
  }).strict();
@@ -99973,6 +100010,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99973
100010
  exports_external.object({
99974
100011
  ...playableInteractionBase,
99975
100012
  type: exports_external.literal("choice"),
100013
+ shuffle: exports_external.boolean(),
99976
100014
  choices: exports_external.array(playableChoiceSchema),
99977
100015
  minChoices: nonnegativeInteger,
99978
100016
  maxChoices: nonnegativeInteger
@@ -99980,6 +100018,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99980
100018
  exports_external.object({
99981
100019
  ...playableInteractionBase,
99982
100020
  type: exports_external.literal("inline-choice"),
100021
+ shuffle: exports_external.boolean(),
99983
100022
  choices: exports_external.array(playableChoiceSchema)
99984
100023
  }).strict(),
99985
100024
  exports_external.object({
@@ -100023,6 +100062,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
100023
100062
  exports_external.object({
100024
100063
  ...playableInteractionBase,
100025
100064
  type: exports_external.literal("gap-match"),
100065
+ shuffle: exports_external.boolean(),
100026
100066
  content: exports_external.string(),
100027
100067
  contentNodes: playableContentNodesSchema,
100028
100068
  gapTexts: exports_external.array(playableGapTextSchema),
@@ -100535,6 +100575,7 @@ function playableChoice(choice) {
100535
100575
  return {
100536
100576
  identifier: choice.identifier,
100537
100577
  content: choice.content,
100578
+ ...qtiAttributeBoolean(choice.attributes, "fixed") ? { fixed: true } : {},
100538
100579
  ...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
100539
100580
  };
100540
100581
  }
@@ -100597,6 +100638,7 @@ function parseInteraction2(questionIdentifier, interaction) {
100597
100638
  responseIdentifier,
100598
100639
  ...prompt,
100599
100640
  choices,
100641
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle"),
100600
100642
  minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
100601
100643
  maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, choices.length)
100602
100644
  };
@@ -100606,7 +100648,8 @@ function parseInteraction2(questionIdentifier, interaction) {
100606
100648
  type,
100607
100649
  responseIdentifier,
100608
100650
  ...prompt,
100609
- choices: requireChoices2(interaction.choices.map(playableChoice))
100651
+ choices: requireChoices2(interaction.choices.map(playableChoice)),
100652
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
100610
100653
  };
100611
100654
  }
100612
100655
  if (type === "text-entry") {
@@ -100667,11 +100710,13 @@ function parseInteraction2(questionIdentifier, interaction) {
100667
100710
  responseIdentifier,
100668
100711
  ...prompt,
100669
100712
  content: interaction.textWithGaps,
100713
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle"),
100670
100714
  contentNodes: interaction.gapContent,
100671
100715
  gapTexts: interaction.gapTexts.map((gapText) => ({
100672
100716
  identifier: gapText.identifier,
100673
100717
  content: gapText.content,
100674
- matchMax: gapText.matchMax
100718
+ matchMax: gapText.matchMax,
100719
+ ...gapText.fixed ? { fixed: true } : {}
100675
100720
  })),
100676
100721
  gaps: [...interaction.gaps],
100677
100722
  maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
@@ -101490,7 +101535,7 @@ function buildInlineChoiceQuestionXml(input, identifier, title) {
101490
101535
  if (!envelope) {
101491
101536
  return null;
101492
101537
  }
101493
- const { structure, responseIdentifier, prompt } = envelope;
101538
+ const { interaction, structure, responseIdentifier, prompt } = envelope;
101494
101539
  const promptParts = prompt.split(INLINE_CHOICE_BLANK);
101495
101540
  if (promptParts.length !== 2) {
101496
101541
  throw new ValidationError("Inline-choice questions require exactly one blank");
@@ -101504,7 +101549,7 @@ function buildInlineChoiceQuestionXml(input, identifier, title) {
101504
101549
  const choicesXml = choices.map((choice) => ` <qti-inline-choice identifier="${escapeXml(choice.identifier)}">${escapeXml(choice.content)}</qti-inline-choice>`).join(`
101505
101550
  `);
101506
101551
  const [before = "", after = ""] = promptParts;
101507
- const itemBody = ` <p>${escapeXml(before)}<qti-inline-choice-interaction response-identifier="${escapeXml(responseIdentifier)}">
101552
+ const itemBody = ` <p>${escapeXml(before)}<qti-inline-choice-interaction response-identifier="${escapeXml(responseIdentifier)}" shuffle="${interaction.shuffle === true}">
101508
101553
  ${choicesXml}
101509
101554
  </qti-inline-choice-interaction>${escapeXml(after)}</p>`;
101510
101555
  return buildExactMatchQuestionXml({
@@ -102119,7 +102164,7 @@ async function prepareReviewMappingUpdate(test, questions) {
102119
102164
  const contentRevision = await assessmentContentRevision(test, questions);
102120
102165
  const assessment = {
102121
102166
  identifier: test.identifier,
102122
- contractVersion: 1,
102167
+ contractVersion: PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION,
102123
102168
  contentRevision,
102124
102169
  title: test.title,
102125
102170
  items: questions.questions.map(({ question }) => ({
@@ -102158,6 +102203,7 @@ async function prepareReviewMappingUpdate(test, questions) {
102158
102203
  var PLAYCADEMY_REVIEW_BANK_TITLE = "Review Bank", PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink", PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1, StoredSinkWarningSchema, ReviewBankSinkMarkerSchema;
102159
102204
  var init_timeback_review_mapping_util = __esm(() => {
102160
102205
  init_esm();
102206
+ init_src();
102161
102207
  init_assessment_runtime2();
102162
102208
  init_qti();
102163
102209
  init_timeback3();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.10.1-beta.3",
3
+ "version": "0.10.1-beta.4",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {