From 4bdd6acdb0e5e80377b64ce3a774d8e731fa871c Mon Sep 17 00:00:00 2001 From: Christian Lawson-Perfect Date: Wed, 4 Dec 2024 11:11:17 +0000 Subject: [PATCH] small improvments to mastery The credit for each learning objective is the number of passed topics in that LO divided by the number of topics in that LO. Previously it was dividing by the number of topics in the whole exam. The "current topic" progress item isn't shown when the exam is finished. --- diagnostic_scripts/mastery.jme | 10 +++++----- tests/diagnostic_scripts.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/diagnostic_scripts/mastery.jme b/diagnostic_scripts/mastery.jme index e3506d393..9690e0e54 100644 --- a/diagnostic_scripts/mastery.jme +++ b/diagnostic_scripts/mastery.jme @@ -56,8 +56,8 @@ next_topic (The next topic to assess): available_topics, // Topics that we can move to next: either no dependencies, or all their dependencies have been passed. filter( t -> let( - all_deps_passed, all(topicdict[topicname]["status"]="passed" for: topicname of: t["topic"]["depends_on"]), - all_deps_passed and t["status"]<>"passed" + all_deps_passed, all(topicdict[topicname]["status"] <> "unknown" for: topicname of: t["topic"]["depends_on"]), + all_deps_passed and t["status"]="unknown" ) , topics ), @@ -202,7 +202,7 @@ after_exam_ended (The state after the exam has finished): progress (Summarise the student's progress through the exam): let( - passed_topics, filter(t -> t["status"]="passed", state["topics"]) + passed_topics, filter(t -> t["status"] = "passed", state["topics"]) , num_passed_topics, len(passed_topics) , num_topics, len(state["topics"]) , exam_progress, num_passed_topics/num_topics @@ -212,14 +212,14 @@ progress (Summarise the student's progress through the exam): let( ltopics, filter(t -> lo["name"] in t["topic"]["learning_objectives"], state["topics"]), passed, filter(t -> t["status"]="passed", ltopics), - p, len(passed)/len(topics), + p, len(passed)/len(ltopics), ["name": lo["name"], "progress": p, "credit": p] ) for: lo of: learning_objectives , topic_progress, [["name": "Current topic: {current_topic}", "progress": topic_credit, "credit": topic_credit]] - , topic_progress + , if(state["finished"], [], topic_progress) + lo_progress + [ ["name": translate("control.total"), "progress": exam_progress, "credit": exam_progress] diff --git a/tests/diagnostic_scripts.js b/tests/diagnostic_scripts.js index 95f44eaa7..845a6df5d 100644 --- a/tests/diagnostic_scripts.js +++ b/tests/diagnostic_scripts.js @@ -3,6 +3,6 @@ Numbas.queueScript('diagnostic_scripts',[],function() { "diagnosys": "state (Produces the initial value of the state object): // should be renamed \"initial_state\"\n [\n \"topics\": map(\n [\n \"topic\": topic,\n \"status\": \"unknown\" // \"unknown\", \"passed\", or \"failed\"\n ],\n topic,\n values(topics)\n ),\n \"retries\": 3,\n \"finished\": false,\n ]\n\ntopics_by_objective (A dictionary mapping a learning objective name to a list of indices of topics):\n dict(map(\n let(\n ltopics, values(topics),\n indices, filter(lo[\"name\"] in ltopics[j][\"learning_objectives\"], j, 0..len(ltopics)-1),\n [lo[\"name\"],indices]\n ),\n lo,\n learning_objectives\n ))\n\nunknown_topics (Which topics are still unknown?): \n map(x[\"topic\"],x,filter(x[\"status\"]=\"unknown\",x,state[\"topics\"]))\n\nfirst_topic (The first topic to pick a question on):\n unknown_topics[floor(len(unknown_topics)/2)][\"name\"]\n\nfirst_question (The first question to show the student):\n random(topics[first_topic][\"questions\"])\n\nget_dependents (An expression which gets the topics to update after answering a question):\n expression(\"\"\"\n [target] + flatten(map(eval(get_dependents,[\"target\":t,\"correct\":correct]),t,topics[target][if(correct,\"depends_on\",\"leads_to\")]))\n \"\"\")\n\ncorrect (Did the student get the current question right?):\n current_question[\"credit\"]=1\n\nafter_answering (Update the state after the student answers a question):\n let(\n ntopics, eval(get_dependents,[\"target\":current_topic,\"correct\":correct])\n , nstate, state + ['topics': map(\n if(tstate[\"topic\"][\"name\"] in ntopics, tstate + [\"status\":if(correct,\"passed\",\"failed\")], tstate),\n tstate,\n state[\"topics\"]\n )]\n , nstate\n )\n\naction_retry (Use up one retry and visit the same topic again):\n [\n \"label\": translate(\"diagnostic.use retry\"),\n \"state\": state + [\"retries\": state[\"retries\"]-1],\n \"next_question\": random(topics[current_topic][\"questions\"])\n ]\n\naction_stop (Stop the exam):\n [\n \"label\": translate(\"diagnostic.end test\"),\n \"state\": state,\n \"next_question\": nothing\n ]\n\naction_move_on (Move to the next topic, or end the exam if there are no more):\n let(\n state, after_answering,\n immediate_next_topics, topics[current_topic][if(correct, \"leads_to\", \"depends_on\")],\n unknown_topics, map(x[\"topic\"],x,filter(x[\"status\"]=\"unknown\",x,state[\"topics\"])),\n unknown_immediate_topics, filter(x[\"name\"] in immediate_next_topics,x,unknown_topics),\n next_topics, if(len(unknown_immediate_topics), unknown_immediate_topics, unknown_topics),\n finished, len(next_topics)=0 or state[\"finished\"],\n topic,\n if(not finished,\n next_topics[floor(len(next_topics)/2)][\"name\"]\n ,\n nothing\n ),\n [\n \"label\": translate(\"diagnostic.move to next topic\"),\n \"state\": after_answering,\n \"next_question\": if(not finished, random(topics[topic][\"questions\"]), nothing)\n ]\n )\n\ncan_move_on:\n action_move_on[\"next_question\"]<>nothing\n\nnext_actions (Actions to offer to the student when they ask to move on):\n let(\n feedback, retries_feedback+\"\\n\\n\"+translate(\"diagnostic.next step question\")\n , [\n \"feedback\": feedback,\n \"actions\": if(not correct and state[\"retries\"]>0, [action_retry], []) + if(can_move_on,[action_move_on],[action_stop])\n ]\n )\n\nafter_exam_ended (Update the state after the exam ends):\n let(\n state, after_answering,\n ntopics, map(t+[\"status\": if(t[\"status\"]=\"unknown\",\"failed\",t[\"status\"])],t,state[\"topics\"]),\n state+[\"finished\": true]\n )\n\nfinished (Is the test finished? True if there are no unknown topics):\n len(unknown_topics)=0 or state[\"finished\"]\n\ntotal_progress:\n let(\n num_topics, len(state[\"topics\"]),\n known, filter(tstate[\"status\"]<>\"unknown\",tstate,state[\"topics\"]),\n passed, filter(tstate[\"status\"]=\"passed\",tstate,known),\n num_known, len(known),\n num_passed, len(passed),\n [\n \"name\": translate(\"control.total\"),\n \"progress\": if(num_topics>0,num_known/num_topics,0), \n \"credit\": if(num_known>0,num_passed/num_topics,0)\n ]\n )\n\nlearning_objective_progress:\n map(\n let(\n tstates, map(state[\"topics\"][j],j,topics_by_objective[lo[\"name\"]]),\n known, filter(tstate[\"status\"]<>\"unknown\",tstate,tstates),\n passed, filter(tstate[\"status\"]=\"passed\",tstate,known),\n num_topics, len(tstates),\n num_known, len(known),\n num_passed, len(passed),\n [\"name\": lo[\"name\"], \"progress\": if(num_topics>0,num_known/num_topics,0), \"credit\": if(finished,num_passed/num_topics,if(num_known>0,num_passed/num_known,0))]\n ),\n lo,\n learning_objectives\n )\n\nprogress (Progress on each of the learning objectives, plus total progress):\n learning_objective_progress+\n total_progress\n\nretries_feedback:\n translate(\"diagnostic.now assessing topic\", [\"current_topic\": current_topic]) + \" \" +\n let(\n retries, state[\"retries\"], \n pluralise(retries, translate(\"diagnostic.one retry left\"), translate(\"diagnostic.retries left\", [\"retries\": retries ]))\n )\n + \" \" +\n let(\n p,total_progress[\"progress\"],\n percentage, dpformat(100p, 0),\n translate(\"diagnostic.percentage completed\", [\"percentage\": percentage])\n )\n\nweak_objective_threshold (The amount of credit below which a learning objective is considered weak):\n 0.6\n\nfinished_feedback:\n let(\n weak_objectives, filter(p[\"credit\"] (if(predicate(x), action(x), x) for: x of: list))\n\n\nquestion_queue_for_topic (When starting a topic, this function makes a queue of questions which must be answered):\n (topic) -> (\n [\"question\": q, \"status\": \"unknown\"]\n for: q\n of: topic[\"topic\"][\"questions\"]\n )\n\n\nstart_topic (A function to update the state, setting the current topic and filling the question queue from that topic):\n (state,topic) -> \n merge(\n state,\n [\n \"current_topic\": topic,\n \"question_queue\": question_queue_for_topic(topic)\n ]\n )\n\n\nget_next_question (A function to get the next question from the queue):\n (state) -> \n let(\n queue, state[\"question_queue\"],\n\n if(len(queue)>0,\n queue[0][\"question\"], \n nothing\n )\n )\n\n\nnext_topic (The next topic to assess):\n (state) ->\n let(\n topics, state[\"topics\"], // List of the state object for each topic\n\n topicdict, dict([t[\"topic\"][\"name\"],t] for: t of: topics), // A mapping from topic names to topic state objects\n\n available_topics, // Topics that we can move to next: either no dependencies, or all their dependencies have been passed.\n filter(\n t -> let(\n all_deps_passed, all(topicdict[topicname][\"status\"]=\"passed\" for: topicname of: t[\"topic\"][\"depends_on\"]),\n all_deps_passed and t[\"status\"]<>\"passed\"\n )\n , topics\n ),\n\n if(len(available_topics)>0,available_topics[0],nothing)\n )\n\n\n/////////////////////\n// Initial variables\n/////////////////////\n\nfirst_topic (The first topic to assess):\n // Picks the first topic which doesn't depend on anything.\n let(\n topics, pre_state[\"topics\"],\n filter(t -> len(t[\"topic\"][\"depends_on\"])=0, topics)[0]\n )\n\n\nfirst_question (The first question to show the student):\n get_next_question(state)\n\n\npre_state (A template for the `state` variable, which will be filled in with the chosen start topic):\n [\n \"topics\": // For each topic, both the given info about that topic and a status, either \"passed\" or \"unknown\".\n [\n \"topic\": topic,\n \"status\": if(len(topic[\"questions\"])=0,\"passed\",\"unknown\") // A topic is \"passed\" if there are no questions left unasked.\n ]\n for: topic\n of: values(topics)\n ,\n \"finished\": false // Is the exam over?\n ]\n\n\nstate (The initial state variable):\n start_topic(pre_state, first_topic)\n\n\n/////////////////////////////\n// Notes used when moving on\n/////////////////////////////\n\ncorrect (Did the student get the current question right?):\n current_question[\"credit\"]=1\n\n\nafter_answering (The state after the student answers a question):\n let(\n queue, state[\"question_queue\"],\n\n nquestion, \n // Set the status of this question in the queue.\n merge(\n queue[0],\n [\"status\": if(correct,\"passed\",\"failed\")]\n ), \n\n nqueue, \n // Change the queue: either remove the current question if correct, or add it to the end.\n queue[1..len(queue)] + if(correct,[],[nquestion]), \n\n ntopics,\n // Update the list of topics, setting the current topic to \"passed\" if the queue is now empty.\n if(len(nqueue)=0,\n update_where(t -> t=state[\"current_topic\"], t -> t+[\"status\": \"passed\"], state[\"topics\"]),\n state[\"topics\"]\n ),\n\n merge(\n // Return a new state with the new list of topics and question queue\n state,\n [\"topics\": ntopics, \"question_queue\": nqueue]\n )\n )\n\n\n///////////\n// Actions\n///////////\n\naction_next_question_same_topic (Move to the next question in the queue):\n [\n \"label\": translate(\"diagnostic.move to next question in topic\"),\n \"state\": after_answering,\n \"next_question\": get_next_question(after_answering)\n ]\n\naction_next_topic (Move to the next topic):\n let(\n state, after_answering, // Start with the state we get from answering the question.\n\n topic, next_topic(state), // Pick a new topic.\n\n nstate, \n if(topic <> nothing, \n start_topic(state, topic) // Update the state with the new topic.\n , \n state // Otherwise, there's no next topic, so this action won't be used.\n ),\n\n [\n \"label\": translate(\"diagnostic.move to next topic\"),\n \"state\": nstate,\n \"next_question\": get_next_question(nstate)\n ]\n )\n\nnext_actions (The list of possible actions after answering a question):\n let(\n state, after_answering,\n queue_empty, len(state[\"question_queue\"])=0,\n actions, \n switch(\n not queue_empty,\n [action_next_question_same_topic] // Move to the next question in the queue\n , next_topic(state) <> nothing,\n [action_next_topic] // Move to the next topic\n ,\n [] // End the exam\n ),\n\n [\n \"feedback\": \"\",\n \"actions\": actions\n ]\n )\n\nafter_exam_ended (The state after the exam has finished):\n merge(\n after_answering,\n [\"finished\": true]\n )\n\n\n//////////////////\n// Feedback notes\n//////////////////\n\nprogress (Summarise the student's progress through the exam):\n let(\n passed_topics, filter(t -> t[\"status\"]=\"passed\", state[\"topics\"])\n , num_passed_topics, len(passed_topics)\n , num_topics, len(state[\"topics\"])\n , exam_progress, num_passed_topics/num_topics\n , topic_credit, 1-len(state[\"question_queue\"])/len(state[\"current_topic\"][\"topic\"][\"questions\"])\n , current_topic, state[\"current_topic\"][\"topic\"][\"name\"]\n , lo_progress,\n let(\n ltopics, filter(t -> lo[\"name\"] in t[\"topic\"][\"learning_objectives\"], state[\"topics\"]),\n passed, filter(t -> t[\"status\"]=\"passed\", ltopics),\n p, len(passed)/len(topics),\n [\"name\": lo[\"name\"], \"progress\": p, \"credit\": p]\n )\n for: lo\n of: learning_objectives\n , topic_progress, [[\"name\": \"Current topic: {current_topic}\", \"progress\": topic_credit, \"credit\": topic_credit]]\n\n , topic_progress \n + lo_progress\n + [\n [\"name\": translate(\"control.total\"), \"progress\": exam_progress, \"credit\": exam_progress]\n ]\n )\n\nfeedback (A text description of the current state): \n if(state[\"finished\"],\n translate(\"diagnostic.complete\")\n ,\n translate(\"diagnostic.studying topic\", [\"topic\": state[\"current_topic\"][\"topic\"][\"name\"]])\n )\n\n" +"// Mastery diagnostic script\n// The student must answer every question correctly.\n// They start with a topic that has no dependencies.\n// After answering a question, if they get it correct, it's done forever.\n// If it's incorrect, the question is put on the end of that topic's \"queue\", \n// so they'll be asked it again later.\n// Once all the questions in the topic are answered correctly, the next topic\n// with no unmet dependencies is picked.\n\n//////////////\n// Functions\n//////////////\n\nupdate_where (Update items in a list which satisfy the given predicate, applying the given function to them):\n ((predicate, action, list) -> (if(predicate(x), action(x), x) for: x of: list))\n\n\nquestion_queue_for_topic (When starting a topic, this function makes a queue of questions which must be answered):\n (topic) -> (\n [\"question\": q, \"status\": \"unknown\"]\n for: q\n of: topic[\"topic\"][\"questions\"]\n )\n\n\nstart_topic (A function to update the state, setting the current topic and filling the question queue from that topic):\n (state,topic) -> \n merge(\n state,\n [\n \"current_topic\": topic,\n \"question_queue\": question_queue_for_topic(topic)\n ]\n )\n\n\nget_next_question (A function to get the next question from the queue):\n (state) -> \n let(\n queue, state[\"question_queue\"],\n\n if(len(queue)>0,\n queue[0][\"question\"], \n nothing\n )\n )\n\n\nnext_topic (The next topic to assess):\n (state) ->\n let(\n topics, state[\"topics\"], // List of the state object for each topic\n\n topicdict, dict([t[\"topic\"][\"name\"],t] for: t of: topics), // A mapping from topic names to topic state objects\n\n available_topics, // Topics that we can move to next: either no dependencies, or all their dependencies have been passed.\n filter(\n t -> let(\n all_deps_passed, all(topicdict[topicname][\"status\"] <> \"unknown\" for: topicname of: t[\"topic\"][\"depends_on\"]),\n all_deps_passed and t[\"status\"]=\"unknown\"\n )\n , topics\n ),\n\n if(len(available_topics)>0,available_topics[0],nothing)\n )\n\n\n/////////////////////\n// Initial variables\n/////////////////////\n\nfirst_topic (The first topic to assess):\n // Picks the first topic which doesn't depend on anything.\n let(\n topics, pre_state[\"topics\"],\n filter(t -> len(t[\"topic\"][\"depends_on\"])=0, topics)[0]\n )\n\n\nfirst_question (The first question to show the student):\n get_next_question(state)\n\n\npre_state (A template for the `state` variable, which will be filled in with the chosen start topic):\n [\n \"topics\": // For each topic, both the given info about that topic and a status, either \"passed\" or \"unknown\".\n [\n \"topic\": topic,\n \"status\": if(len(topic[\"questions\"])=0,\"passed\",\"unknown\") // A topic is \"passed\" if there are no questions left unasked.\n ]\n for: topic\n of: values(topics)\n ,\n \"finished\": false // Is the exam over?\n ]\n\n\nstate (The initial state variable):\n start_topic(pre_state, first_topic)\n\n\n/////////////////////////////\n// Notes used when moving on\n/////////////////////////////\n\ncorrect (Did the student get the current question right?):\n current_question[\"credit\"]=1\n\n\nafter_answering (The state after the student answers a question):\n let(\n queue, state[\"question_queue\"],\n\n nquestion, \n // Set the status of this question in the queue.\n merge(\n queue[0],\n [\"status\": if(correct,\"passed\",\"failed\")]\n ), \n\n nqueue, \n // Change the queue: either remove the current question if correct, or add it to the end.\n queue[1..len(queue)] + if(correct,[],[nquestion]), \n\n ntopics,\n // Update the list of topics, setting the current topic to \"passed\" if the queue is now empty.\n if(len(nqueue)=0,\n update_where(t -> t=state[\"current_topic\"], t -> t+[\"status\": \"passed\"], state[\"topics\"]),\n state[\"topics\"]\n ),\n\n merge(\n // Return a new state with the new list of topics and question queue\n state,\n [\"topics\": ntopics, \"question_queue\": nqueue]\n )\n )\n\n\n///////////\n// Actions\n///////////\n\naction_next_question_same_topic (Move to the next question in the queue):\n [\n \"label\": translate(\"diagnostic.move to next question in topic\"),\n \"state\": after_answering,\n \"next_question\": get_next_question(after_answering)\n ]\n\naction_next_topic (Move to the next topic):\n let(\n state, after_answering, // Start with the state we get from answering the question.\n\n topic, next_topic(state), // Pick a new topic.\n\n nstate, \n if(topic <> nothing, \n start_topic(state, topic) // Update the state with the new topic.\n , \n state // Otherwise, there's no next topic, so this action won't be used.\n ),\n\n [\n \"label\": translate(\"diagnostic.move to next topic\"),\n \"state\": nstate,\n \"next_question\": get_next_question(nstate)\n ]\n )\n\nnext_actions (The list of possible actions after answering a question):\n let(\n state, after_answering,\n queue_empty, len(state[\"question_queue\"])=0,\n actions, \n switch(\n not queue_empty,\n [action_next_question_same_topic] // Move to the next question in the queue\n , next_topic(state) <> nothing,\n [action_next_topic] // Move to the next topic\n ,\n [] // End the exam\n ),\n\n [\n \"feedback\": \"\",\n \"actions\": actions\n ]\n )\n\nafter_exam_ended (The state after the exam has finished):\n merge(\n after_answering,\n [\"finished\": true]\n )\n\n\n//////////////////\n// Feedback notes\n//////////////////\n\nprogress (Summarise the student's progress through the exam):\n let(\n passed_topics, filter(t -> t[\"status\"] = \"passed\", state[\"topics\"])\n , num_passed_topics, len(passed_topics)\n , num_topics, len(state[\"topics\"])\n , exam_progress, num_passed_topics/num_topics\n , topic_credit, 1-len(state[\"question_queue\"])/len(state[\"current_topic\"][\"topic\"][\"questions\"])\n , current_topic, state[\"current_topic\"][\"topic\"][\"name\"]\n , lo_progress,\n let(\n ltopics, filter(t -> lo[\"name\"] in t[\"topic\"][\"learning_objectives\"], state[\"topics\"]),\n passed, filter(t -> t[\"status\"]=\"passed\", ltopics),\n p, len(passed)/len(ltopics),\n [\"name\": lo[\"name\"], \"progress\": p, \"credit\": p]\n )\n for: lo\n of: learning_objectives\n , topic_progress, [[\"name\": \"Current topic: {current_topic}\", \"progress\": topic_credit, \"credit\": topic_credit]]\n\n , if(state[\"finished\"], [], topic_progress)\n + lo_progress\n + [\n [\"name\": translate(\"control.total\"), \"progress\": exam_progress, \"credit\": exam_progress]\n ]\n )\n\nfeedback (A text description of the current state): \n if(state[\"finished\"],\n translate(\"diagnostic.complete\")\n ,\n translate(\"diagnostic.studying topic\", [\"topic\": state[\"current_topic\"][\"topic\"][\"name\"]])\n )\n\n" }; });