Add full-length Step 3 Day 1 simulation

This commit is contained in:
hz
2026-07-14 16:54:11 -05:00
parent 8aaf7d61e9
commit 206a5c0124
19 changed files with 1520 additions and 32 deletions
+169
View File
@@ -0,0 +1,169 @@
extends VBoxContainer
const Style := preload("res://scripts/ui/ui_style.gd")
var main: Control
var score_label: Label
var summary_label: Label
var category_list: VBoxContainer
var review_list: VBoxContainer
var outcome_score: SpinBox
var outcome_date: LineEdit
var outcome_consent: CheckBox
var outcome_status: Label
var outcome_delete_button: Button
func _ready() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
add_theme_constant_override("separation", 12)
var eyebrow := Label.new()
eyebrow.text = "STEP 3 DAY 1 SIMULATION COMPLETE"
Style.set_label(eyebrow, 14, Style.CYAN)
add_child(eyebrow)
score_label = Label.new()
Style.set_label(score_label, 36, Style.LIME)
add_child(score_label)
summary_label = Label.new()
summary_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(summary_label, 17, Style.TEXT)
add_child(summary_label)
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(scroll)
var content := VBoxContainer.new()
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content.add_theme_constant_override("separation", 10)
scroll.add_child(content)
var category_heading := Label.new()
category_heading.text = "Performance by content area"
Style.set_label(category_heading, 22, Style.TEXT)
content.add_child(category_heading)
category_list = VBoxContainer.new()
content.add_child(category_list)
var review_heading := Label.new()
review_heading.text = "Answer review"
Style.set_label(review_heading, 22, Style.TEXT)
content.add_child(review_heading)
review_list = VBoxContainer.new()
review_list.add_theme_constant_override("separation", 8)
content.add_child(review_list)
var outcome_heading := Label.new()
outcome_heading.text = "Help calibrate future predictions"
Style.set_label(outcome_heading, 22, Style.TEXT)
content.add_child(outcome_heading)
var outcome_note := Label.new()
outcome_note.text = "After receiving your official result, you may voluntarily submit it. You can edit or delete it at any time; calibration uses de-identified aggregates."
outcome_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(outcome_note, 15, Style.MUTED)
content.add_child(outcome_note)
var outcome_row := HBoxContainer.new()
outcome_row.add_theme_constant_override("separation", 10)
content.add_child(outcome_row)
outcome_score = SpinBox.new()
outcome_score.min_value = 1
outcome_score.max_value = 300
outcome_score.value = 200
outcome_score.custom_minimum_size = Vector2(120, 44)
outcome_row.add_child(outcome_score)
outcome_date = LineEdit.new()
outcome_date.placeholder_text = "Exam date: YYYY-MM-DD"
outcome_date.custom_minimum_size = Vector2(220, 44)
outcome_row.add_child(outcome_date)
outcome_consent = CheckBox.new()
outcome_consent.text = "I consent to calibration use"
outcome_row.add_child(outcome_consent)
var save_outcome := Button.new()
save_outcome.text = "Save outcome"
Style.style_chip(save_outcome, Style.CYAN)
save_outcome.pressed.connect(_save_outcome)
outcome_row.add_child(save_outcome)
outcome_delete_button = Button.new()
outcome_delete_button.text = "Delete outcome"
Style.style_chip(outcome_delete_button, Style.RED)
outcome_delete_button.pressed.connect(ExamSimulationService.delete_outcome)
outcome_row.add_child(outcome_delete_button)
outcome_status = Label.new()
Style.set_label(outcome_status, 15, Style.MUTED)
content.add_child(outcome_status)
if not ExamSimulationService.outcome_changed.is_connected(_refresh_outcome):
ExamSimulationService.outcome_changed.connect(_refresh_outcome)
var done := Button.new()
done.text = "Return to study modes"
done.custom_minimum_size = Vector2(240, 50)
done.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_primary_button(done)
done.pressed.connect(func(): main.show_screen("selection", "step3"))
add_child(done)
func on_show(payload: Variant = null) -> void:
var simulation: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else ExamSimulationService.simulation
var score: Dictionary = simulation.get("score", {})
var predicted := int(score.get("predictedScore", 0))
var low := int(score.get("rangeLow", 0))
var high := int(score.get("rangeHigh", 0))
var probability := int(round(float(score.get("passProbability", 0.0)) * 100.0))
score_label.text = "Predicted Step 3 score: %d (%d-%d)" % [predicted, low, high]
summary_label.text = "%d/%d correct | Estimated probability of reaching 200: %d%% | Previously seen questions: %d\nThis provisional educational estimate is not an official USMLE or NBME score and excludes CCS performance." % [
int(score.get("correct", 0)), int(score.get("total", 0)), probability, int(score.get("seenCount", 0))]
_clear(category_list)
for category in score.get("categories", {}):
var value: Dictionary = score["categories"][category]
var label := Label.new()
label.text = "%s: %d/%d" % [category, int(value.get("correct", 0)), int(value.get("total", 0))]
Style.set_label(label, 16, Style.TEXT)
category_list.add_child(label)
_build_review(simulation)
ExamSimulationService.load_outcome()
func _build_review(simulation: Dictionary) -> void:
_clear(review_list)
var responses := {}
for response in simulation.get("responses", []):
responses[str(response.get("questionExternalId", ""))] = response
for raw_question in simulation.get("reviewQuestions", []):
if typeof(raw_question) != TYPE_DICTIONARY:
continue
var question: Dictionary = raw_question
var id := str(question.get("externalId", ""))
var selected := str(responses.get(id, {}).get("choiceId", ""))
var correct := str(question.get("correctChoiceId", ""))
var item := RichTextLabel.new()
item.bbcode_enabled = true
item.fit_content = true
item.custom_minimum_size = Vector2(0, 84)
item.text = "[b]%s[/b] %s\nSelected: %s | Correct: %s\n%s" % ["CORRECT" if selected == correct else "MISS", str(question.get("leadIn", "")), selected if selected != "" else "Unanswered", correct, str(question.get("explanation", ""))]
review_list.add_child(item)
func _clear(container: Control) -> void:
for child in container.get_children():
child.queue_free()
func _save_outcome() -> void:
if not outcome_consent.button_pressed:
outcome_status.text = "Consent is required before saving an outcome."
return
if outcome_date.text.length() != 10:
outcome_status.text = "Enter the exam date as YYYY-MM-DD."
return
ExamSimulationService.save_outcome(int(outcome_score.value), outcome_date.text)
outcome_status.text = "Saving..."
func _refresh_outcome() -> void:
if ExamSimulationService.outcome.is_empty():
outcome_status.text = "No official outcome is stored."
outcome_delete_button.visible = false
return
outcome_score.value = int(ExamSimulationService.outcome.get("officialScore", 200))
outcome_date.text = str(ExamSimulationService.outcome.get("examDate", "")).left(10)
outcome_consent.button_pressed = true
outcome_delete_button.visible = true
outcome_status.text = "Official outcome saved."