A voice game: converse-driven game loop¶
In a nutshell
You build a ConversationalGameSkill that runs a full voice game on top of OCP, using on_play_game/on_game_command so free-form input reaches game logic instead of the intent parser.
When you'd want this: the skill runs a game entirely by voice, such as a number-guessing game or a text adventure. While the game is playing, almost anything the user says is game input, not a new command, and the skill must still respond correctly to "stop" or "pause".
ConversationalGameSkill (from ovos_workshop.skills.game_skill) is a small set of on_* hooks over OVOSCommonPlaybackSkill. The OCP media pipeline treats a game as MediaType.GAME: saying "play guess the number" reaches the skill through the same "play X" matching as music or a podcast, and OCP's play/pause/resume/stop transport controls double as the game's transport.
import random
from ovos_workshop.decorators import intent_handler
from ovos_workshop.skills.game_skill import ConversationalGameSkill
class GuessNumberGameSkill(ConversationalGameSkill):
def __init__(self, *args, **kwargs):
super().__init__(skill_voc_filename="GuessNumberGameKeyword",
*args, **kwargs)
self.secret = None
self.guesses_left = 0
def on_play_game(self):
# the framework already set self.is_playing True before this runs
self.secret = random.randint(1, 100)
self.guesses_left = 7
self.speak_dialog("game_start", {"tries": self.guesses_left})
def on_stop_game(self):
self.speak_dialog("game_stop")
def on_abandon_game(self):
# called before on_stop_game if the user goes quiet mid-game
self.speak_dialog("game_abandoned")
def on_game_command(self, utterance: str, lang: str):
# every utterance that isn't claimed by an intent below lands here
try:
guess = int(utterance)
except ValueError:
self.speak_dialog("not_a_number", expect_response=True)
return
self.guesses_left -= 1
if guess == self.secret:
self.speak_dialog("guess_correct")
self.stop_game()
elif self.guesses_left <= 0:
self.speak_dialog("guess_out_of_tries", {"answer": self.secret})
self.stop_game()
elif guess < self.secret:
self.speak_dialog("guess_higher", {"tries": self.guesses_left},
expect_response=True)
else:
self.speak_dialog("guess_lower", {"tries": self.guesses_left},
expect_response=True)
@intent_handler("cheat_hint.intent")
def handle_cheat_hint(self, message):
# this is the ONLY declared intent in the whole skill; every other
# utterance while playing falls through to on_game_command above,
# keeping the game's own vocabulary out of the intent parser entirely
if self.is_playing:
self.speak_dialog("hint_refused")
Moving parts¶
ConversationalGameSkillsubclassesOVOSGameSkill, which subclassesOVOSCommonPlaybackSkill. The constructor requiresskill_voc_filename, a.vocfile (locale/en-us/vocab/GuessNumberGameKeyword.voc) listing the game's name, so OCP's search step recognizes "play guess the number" as this skill and not a music query.on_play_game(),on_stop_game(), andon_game_command()are the abstract hooks every game must implement.on_save_game()/on_load_game()are optional overrides that default to speaking a "can't save"/"can't load" dialog if left alone. OCP callson_play_game()after already marking the skill as playing, soself.is_playingisTrueinside it.on_pause_game()andon_resume_game()also come from the base class with a working default: an acknowledgement sound, plus an optional dialog gated by thepause_dialogsetting. Override them only if pausing needs game-specific behavior.on_game_command(utterance, lang)is where free-form game input arrives. The base class'sconverse()calls it whenever the skill is playing and not paused, and the utterance would not otherwise trigger one of this skill's own@intent_handlers (checked viaskill_will_trigger). That is what "keeping intents inert during play" means in practice. Declare as few@intent_handlers as the game truly needs, such ascheat_hint.intentabove. Everything else reacheson_game_command: digits, "up", "north", whatever the game vocabulary is. Without this, those utterances would miss every intent and fall through to a fallback skill.self.stop_game()is the base class helper a skill calls to end the game itself, after a correct guess or running out of tries. It clears playback state and then callson_stop_game()for you. Do not callon_stop_game()directly.- Abort/inactivity is handled above the skill entirely: if the user goes quiet for a while, the intent service deactivates the skill, which calls
on_abandon_game()and thenstop_game()(which in turn callson_stop_game()). Explicit "stop" while playing goes through the normal OCP stop transport, which callsstop_game()the same way. - Set
self.settings["auto_save"] = Trueand implementon_save_game()for autosave-before-stop behavior.ConversationalGameSkillthen calls it for you on everyconverse()turn and onstop(), when both the setting and the override are present (checked viasave_is_implemented).
Full production example
skill-moon-game (a VoiceGamez title, not currently public) is a full ConversationalGameSkill: a branching narrative driven entirely through on_game_command, with IntentLayers gating which branch-specific phrases are even considered at each story beat. FrotzSkill (from the pyfrotz package, a VoiceGamez codebase that is not currently public) shows the same hooks wrapping something entirely different: every utterance in on_game_command is piped as a raw command to an external Z-machine interpreter process, and its text output is what gets spoken.
Read next: Skill Cookbook Related: A small local media playlist · OCP Skills · Continuous conversation