Neocortex
IntegrationsGodot SDK

Quiz

Run a spoken quiz, game show or test in Godot 4 with a Neocortex AI character as the host, for one player or several, using the Quiz Agent and a question set.

Team plan

Quizzes need a Team plan and at least one question set.

NeocortexQuizAgent lets one of your characters host a quiz, a game show or a spoken test. You write the questions in a question set on the web platform. Question order, retries, hints, judging and scoring happen on the server. Your game hands over what the player said and shows what comes back.

How a run works on the server is covered in Web API Quiz.

Setting up

  • Add a NeocortexQuizAgent node
  • Pick the host in its character_id dropdown and the set in its question_set_id dropdown. Both lists come from your account.
  • Optional: set question_count (0 uses the set's own count) and question_order (SET_DEFAULT, IN_ORDER or SHUFFLED)

The host speaks in the character's voice. Turn off speak_host_lines for a silent host. An AudioStreamPlayer is created for the voice when you don't assign an audio_player.

For a quick text UI, add a NeocortexQuizUI beside the agent, with a chat panel, a text input and a thinking indicator under it. The input only shows while the host is waiting for an answer.

Running a quiz

quiz_scene.gd
extends Node

@onready var quiz: NeocortexQuizAgent = $NeocortexQuizAgent

func _ready() -> void:
	quiz.host_line.connect(func(line: NeocortexQuizLine): subtitles.show_line(line.text))
	quiz.question_changed.connect(_show_question)
	quiz.upcoming_question.connect(func(next): scenery.preload_scene(next.keyword))
	quiz.leaderboard_changed.connect(func(standings: Array): scoreboard.show_standings(standings))
	quiz.quiz_finished.connect(func(standings: Array): results.show_results(standings))
	quiz.begin()

func _show_question(question: NeocortexQuizQuestion) -> void:
	question_label.text = question.question_text
	scenery.show_scene(question.keyword)
	for option in question.options:
		var button := Button.new()
		button.text = option.text
		button.pressed.connect(quiz.choose.bind(option.id))
		options_box.add_child(button)

begin() starts the run. The host welcomes the player and asks the first question. After that your game only reports what the player did:

Player didCall
Tapped an optionchoose(option_id)
Spokesay(transcript)
Typedsay(text, true)
Skipped, or your timer ran outpass_turn()

Hints, repeats and stopping are just words: say("Can I have a hint?"). last_intent tells you how the host read the last thing said, for example HINT or REPEAT. A typed answer is judged as written, so a misspelling is not treated as a mishearing.

question_changed fires as the host starts asking the question, so the scene changes on the right line. upcoming_question gives the next question's keyword early, so you can load its scene while the host is still talking.

The question

NeocortexQuizQuestion carries what the game may show, and nothing it may not: the answer, the rubric and the hint text stay on the server.

MemberDescription
question_textThe question.
type"free_text", "multiple_choice" or "true_false".
optionsThe choices to show, each with id and text. Empty for an open answer.
is_choice()True when the player picks from options.
keywordThe author's name for the scene this question needs, such as SHOW_RED_APPLE.
has_hintWhether the player can ask for a clue.

Voice answers

Open the microphone when the host expects something, transcribe the recording, and pass the text to say():

Voice answers
@onready var mic: NeocortexAudioReceiver = $NeocortexAudioReceiver
var api := ApiRequest.new()

func _ready() -> void:
	add_child(api)
	quiz.expecting_changed.connect(func(expecting: NeocortexEnums.QuizExpecting):
		if expecting != NeocortexEnums.QuizExpecting.NOTHING:
			mic.start_microphone())
	mic.audio_recorded.connect(_on_recorded)

func _on_recorded(wav: AudioStreamWAV) -> void:
	var text := await api.request_transcription(quiz.character_id, wav)
	if text.strip_edges().is_empty():
		mic.start_microphone() # nothing heard, listen again
	else:
		quiz.say(text)

expecting is ANSWER for an attempt at the question, CONFIRMATION for a yes or a no (usually to an offer of another try), and NOTHING while the host is talking or the quiz is over.

Don't send an empty transcript with say(""). An empty string is a deliberate pass, which means the player chose to skip.

Several players

Fill participants in the Inspector, or pass a roster to begin(), and name the player on each answer:

Several players
quiz.begin([
	NeocortexQuizParticipant.create("anna", "Anna"),
	NeocortexQuizParticipant.create("ben", "Ben"),
])

quiz.choose_as("anna", option_id)
quiz.say_as("ben", transcript)
quiz.pass_as("ben") # Ben's timer ran out

The host waits until everyone has answered. pending_changed and pending_participants track who is still to answer, and answered_participants who has. Timers are yours. The Quiz Show sample runs one to four players.

result raises how each player's answer was judged (participant_id, verdict, score), and leaderboard_changed the standings (participant_id, display_name, total_score, rank). Players on the same score share a rank.

Input while the host is busy

While a turn is in flight, is_busy is true and busy_changed fires. With queue_input_while_busy on (the default), what the player says is held and sent once the host is ready. Only the newest answer per player is kept, and an answer older than max_pending_input_age seconds (default 4) is dropped.

input_dropped(text, reason) tells you when something never reached the host:

ReasonWhy
SUPERSEDEDThe player said something newer, which is sent instead.
EXPIREDIt waited too long to still answer what the host asked.
FINISHEDThe run has already ended.
NO_SESSIONThe quiz has not started yet.
FAILEDThe turn it would have gone on failed.
BUSYThe host was busy and queue_input_while_busy is off.

Errors and recovery

When a turn fails, the agent drops held input and calls resync() by itself, which reads the run back from the server and reopens input where it left off. Call resync() yourself after a lost connection.

When your team has run out of usage, or a usage limit is reached, the agent raises credits_exhausted(code) and keeps input closed until you call resync().

abort() silences the host and abandons whatever is in flight, without ending the run on the server.

The full list of members and signals is in the API Reference.

On this page